##// END OF EJS Templates
Adds firstname initials+lastname user format....
Jean-Philippe Lang -
r12415:6e6c6fac5caf
parent child
Show More
@@ -1,745 +1,750
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 :firstinitial_lastname => {
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 :order => %w(firstname lastname id),
38 :setting_order => 2
39 },
35 40 :firstname => {
36 41 :string => '#{firstname}',
37 42 :order => %w(firstname id),
38 43 :setting_order => 3
39 44 },
40 45 :lastname_firstname => {
41 46 :string => '#{lastname} #{firstname}',
42 47 :order => %w(lastname firstname id),
43 48 :setting_order => 4
44 49 },
45 50 :lastname_coma_firstname => {
46 51 :string => '#{lastname}, #{firstname}',
47 52 :order => %w(lastname firstname id),
48 53 :setting_order => 5
49 54 },
50 55 :lastname => {
51 56 :string => '#{lastname}',
52 57 :order => %w(lastname id),
53 58 :setting_order => 6
54 59 },
55 60 :username => {
56 61 :string => '#{login}',
57 62 :order => %w(login id),
58 63 :setting_order => 7
59 64 },
60 65 }
61 66
62 67 MAIL_NOTIFICATION_OPTIONS = [
63 68 ['all', :label_user_mail_option_all],
64 69 ['selected', :label_user_mail_option_selected],
65 70 ['only_my_events', :label_user_mail_option_only_my_events],
66 71 ['only_assigned', :label_user_mail_option_only_assigned],
67 72 ['only_owner', :label_user_mail_option_only_owner],
68 73 ['none', :label_user_mail_option_none]
69 74 ]
70 75
71 76 has_and_belongs_to_many :groups,
72 77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
73 78 :after_add => Proc.new {|user, group| group.user_added(user)},
74 79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
75 80 has_many :changesets, :dependent => :nullify
76 81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
77 82 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
78 83 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
79 84 belongs_to :auth_source
80 85
81 86 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
82 87 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
83 88
84 89 acts_as_customizable
85 90
86 91 attr_accessor :password, :password_confirmation, :generate_password
87 92 attr_accessor :last_before_login_on
88 93 # Prevents unauthorized assignments
89 94 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
90 95
91 96 LOGIN_LENGTH_LIMIT = 60
92 97 MAIL_LENGTH_LIMIT = 60
93 98
94 99 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
95 100 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
96 101 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
97 102 # Login must contain letters, numbers, underscores only
98 103 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
99 104 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
100 105 validates_length_of :firstname, :lastname, :maximum => 30
101 106 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
102 107 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
103 108 validates_confirmation_of :password, :allow_nil => true
104 109 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
105 110 validate :validate_password_length
106 111
107 112 before_create :set_mail_notification
108 113 before_save :generate_password_if_needed, :update_hashed_password
109 114 before_destroy :remove_references_before_destroy
110 115 after_save :update_notified_project_ids
111 116
112 117 scope :in_group, lambda {|group|
113 118 group_id = group.is_a?(Group) ? group.id : group.to_i
114 119 where("#{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)
115 120 }
116 121 scope :not_in_group, lambda {|group|
117 122 group_id = group.is_a?(Group) ? group.id : group.to_i
118 123 where("#{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)
119 124 }
120 125 scope :sorted, lambda { order(*User.fields_for_order_statement)}
121 126
122 127 def set_mail_notification
123 128 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
124 129 true
125 130 end
126 131
127 132 def update_hashed_password
128 133 # update hashed_password if password was set
129 134 if self.password && self.auth_source_id.blank?
130 135 salt_password(password)
131 136 end
132 137 end
133 138
134 139 alias :base_reload :reload
135 140 def reload(*args)
136 141 @name = nil
137 142 @projects_by_role = nil
138 143 @membership_by_project_id = nil
139 144 @notified_projects_ids = nil
140 145 @notified_projects_ids_changed = false
141 146 @builtin_role = nil
142 147 base_reload(*args)
143 148 end
144 149
145 150 def mail=(arg)
146 151 write_attribute(:mail, arg.to_s.strip)
147 152 end
148 153
149 154 def identity_url=(url)
150 155 if url.blank?
151 156 write_attribute(:identity_url, '')
152 157 else
153 158 begin
154 159 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
155 160 rescue OpenIdAuthentication::InvalidOpenId
156 161 # Invalid url, don't save
157 162 end
158 163 end
159 164 self.read_attribute(:identity_url)
160 165 end
161 166
162 167 # Returns the user that matches provided login and password, or nil
163 168 def self.try_to_login(login, password, active_only=true)
164 169 login = login.to_s
165 170 password = password.to_s
166 171
167 172 # Make sure no one can sign in with an empty login or password
168 173 return nil if login.empty? || password.empty?
169 174 user = find_by_login(login)
170 175 if user
171 176 # user is already in local database
172 177 return nil unless user.check_password?(password)
173 178 return nil if !user.active? && active_only
174 179 else
175 180 # user is not yet registered, try to authenticate with available sources
176 181 attrs = AuthSource.authenticate(login, password)
177 182 if attrs
178 183 user = new(attrs)
179 184 user.login = login
180 185 user.language = Setting.default_language
181 186 if user.save
182 187 user.reload
183 188 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
184 189 end
185 190 end
186 191 end
187 192 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
188 193 user
189 194 rescue => text
190 195 raise text
191 196 end
192 197
193 198 # Returns the user who matches the given autologin +key+ or nil
194 199 def self.try_to_autologin(key)
195 200 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
196 201 if user
197 202 user.update_column(:last_login_on, Time.now)
198 203 user
199 204 end
200 205 end
201 206
202 207 def self.name_formatter(formatter = nil)
203 208 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
204 209 end
205 210
206 211 # Returns an array of fields names than can be used to make an order statement for users
207 212 # according to how user names are displayed
208 213 # Examples:
209 214 #
210 215 # User.fields_for_order_statement => ['users.login', 'users.id']
211 216 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
212 217 def self.fields_for_order_statement(table=nil)
213 218 table ||= table_name
214 219 name_formatter[:order].map {|field| "#{table}.#{field}"}
215 220 end
216 221
217 222 # Return user's full name for display
218 223 def name(formatter = nil)
219 224 f = self.class.name_formatter(formatter)
220 225 if formatter
221 226 eval('"' + f[:string] + '"')
222 227 else
223 228 @name ||= eval('"' + f[:string] + '"')
224 229 end
225 230 end
226 231
227 232 def active?
228 233 self.status == STATUS_ACTIVE
229 234 end
230 235
231 236 def registered?
232 237 self.status == STATUS_REGISTERED
233 238 end
234 239
235 240 def locked?
236 241 self.status == STATUS_LOCKED
237 242 end
238 243
239 244 def activate
240 245 self.status = STATUS_ACTIVE
241 246 end
242 247
243 248 def register
244 249 self.status = STATUS_REGISTERED
245 250 end
246 251
247 252 def lock
248 253 self.status = STATUS_LOCKED
249 254 end
250 255
251 256 def activate!
252 257 update_attribute(:status, STATUS_ACTIVE)
253 258 end
254 259
255 260 def register!
256 261 update_attribute(:status, STATUS_REGISTERED)
257 262 end
258 263
259 264 def lock!
260 265 update_attribute(:status, STATUS_LOCKED)
261 266 end
262 267
263 268 # Returns true if +clear_password+ is the correct user's password, otherwise false
264 269 def check_password?(clear_password)
265 270 if auth_source_id.present?
266 271 auth_source.authenticate(self.login, clear_password)
267 272 else
268 273 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
269 274 end
270 275 end
271 276
272 277 # Generates a random salt and computes hashed_password for +clear_password+
273 278 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
274 279 def salt_password(clear_password)
275 280 self.salt = User.generate_salt
276 281 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
277 282 end
278 283
279 284 # Does the backend storage allow this user to change their password?
280 285 def change_password_allowed?
281 286 return true if auth_source.nil?
282 287 return auth_source.allow_password_changes?
283 288 end
284 289
285 290 def must_change_password?
286 291 must_change_passwd? && change_password_allowed?
287 292 end
288 293
289 294 def generate_password?
290 295 generate_password == '1' || generate_password == true
291 296 end
292 297
293 298 # Generate and set a random password on given length
294 299 def random_password(length=40)
295 300 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
296 301 chars -= %w(0 O 1 l)
297 302 password = ''
298 303 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
299 304 self.password = password
300 305 self.password_confirmation = password
301 306 self
302 307 end
303 308
304 309 def pref
305 310 self.preference ||= UserPreference.new(:user => self)
306 311 end
307 312
308 313 def time_zone
309 314 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
310 315 end
311 316
312 317 def wants_comments_in_reverse_order?
313 318 self.pref[:comments_sorting] == 'desc'
314 319 end
315 320
316 321 # Return user's RSS key (a 40 chars long string), used to access feeds
317 322 def rss_key
318 323 if rss_token.nil?
319 324 create_rss_token(:action => 'feeds')
320 325 end
321 326 rss_token.value
322 327 end
323 328
324 329 # Return user's API key (a 40 chars long string), used to access the API
325 330 def api_key
326 331 if api_token.nil?
327 332 create_api_token(:action => 'api')
328 333 end
329 334 api_token.value
330 335 end
331 336
332 337 # Return an array of project ids for which the user has explicitly turned mail notifications on
333 338 def notified_projects_ids
334 339 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
335 340 end
336 341
337 342 def notified_project_ids=(ids)
338 343 @notified_projects_ids_changed = true
339 344 @notified_projects_ids = ids
340 345 end
341 346
342 347 # Updates per project notifications (after_save callback)
343 348 def update_notified_project_ids
344 349 if @notified_projects_ids_changed
345 350 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
346 351 members.update_all(:mail_notification => false)
347 352 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
348 353 end
349 354 end
350 355 private :update_notified_project_ids
351 356
352 357 def valid_notification_options
353 358 self.class.valid_notification_options(self)
354 359 end
355 360
356 361 # Only users that belong to more than 1 project can select projects for which they are notified
357 362 def self.valid_notification_options(user=nil)
358 363 # Note that @user.membership.size would fail since AR ignores
359 364 # :include association option when doing a count
360 365 if user.nil? || user.memberships.length < 1
361 366 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
362 367 else
363 368 MAIL_NOTIFICATION_OPTIONS
364 369 end
365 370 end
366 371
367 372 # Find a user account by matching the exact login and then a case-insensitive
368 373 # version. Exact matches will be given priority.
369 374 def self.find_by_login(login)
370 375 if login.present?
371 376 login = login.to_s
372 377 # First look for an exact match
373 378 user = where(:login => login).all.detect {|u| u.login == login}
374 379 unless user
375 380 # Fail over to case-insensitive if none was found
376 381 user = where("LOWER(login) = ?", login.downcase).first
377 382 end
378 383 user
379 384 end
380 385 end
381 386
382 387 def self.find_by_rss_key(key)
383 388 Token.find_active_user('feeds', key)
384 389 end
385 390
386 391 def self.find_by_api_key(key)
387 392 Token.find_active_user('api', key)
388 393 end
389 394
390 395 # Makes find_by_mail case-insensitive
391 396 def self.find_by_mail(mail)
392 397 where("LOWER(mail) = ?", mail.to_s.downcase).first
393 398 end
394 399
395 400 # Returns true if the default admin account can no longer be used
396 401 def self.default_admin_account_changed?
397 402 !User.active.find_by_login("admin").try(:check_password?, "admin")
398 403 end
399 404
400 405 def to_s
401 406 name
402 407 end
403 408
404 409 CSS_CLASS_BY_STATUS = {
405 410 STATUS_ANONYMOUS => 'anon',
406 411 STATUS_ACTIVE => 'active',
407 412 STATUS_REGISTERED => 'registered',
408 413 STATUS_LOCKED => 'locked'
409 414 }
410 415
411 416 def css_classes
412 417 "user #{CSS_CLASS_BY_STATUS[status]}"
413 418 end
414 419
415 420 # Returns the current day according to user's time zone
416 421 def today
417 422 if time_zone.nil?
418 423 Date.today
419 424 else
420 425 Time.now.in_time_zone(time_zone).to_date
421 426 end
422 427 end
423 428
424 429 # Returns the day of +time+ according to user's time zone
425 430 def time_to_date(time)
426 431 if time_zone.nil?
427 432 time.to_date
428 433 else
429 434 time.in_time_zone(time_zone).to_date
430 435 end
431 436 end
432 437
433 438 def logged?
434 439 true
435 440 end
436 441
437 442 def anonymous?
438 443 !logged?
439 444 end
440 445
441 446 # Returns user's membership for the given project
442 447 # or nil if the user is not a member of project
443 448 def membership(project)
444 449 project_id = project.is_a?(Project) ? project.id : project
445 450
446 451 @membership_by_project_id ||= Hash.new {|h, project_id|
447 452 h[project_id] = memberships.where(:project_id => project_id).first
448 453 }
449 454 @membership_by_project_id[project_id]
450 455 end
451 456
452 457 # Returns the user's bult-in role
453 458 def builtin_role
454 459 @builtin_role ||= Role.non_member
455 460 end
456 461
457 462 # Return user's roles for project
458 463 def roles_for_project(project)
459 464 roles = []
460 465 # No role on archived projects
461 466 return roles if project.nil? || project.archived?
462 467 if membership = membership(project)
463 468 roles = membership.roles
464 469 else
465 470 roles << builtin_role
466 471 end
467 472 roles
468 473 end
469 474
470 475 # Return true if the user is a member of project
471 476 def member_of?(project)
472 477 projects.to_a.include?(project)
473 478 end
474 479
475 480 # Returns a hash of user's projects grouped by roles
476 481 def projects_by_role
477 482 return @projects_by_role if @projects_by_role
478 483
479 484 @projects_by_role = Hash.new([])
480 485 memberships.each do |membership|
481 486 if membership.project
482 487 membership.roles.each do |role|
483 488 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
484 489 @projects_by_role[role] << membership.project
485 490 end
486 491 end
487 492 end
488 493 @projects_by_role.each do |role, projects|
489 494 projects.uniq!
490 495 end
491 496
492 497 @projects_by_role
493 498 end
494 499
495 500 # Returns true if user is arg or belongs to arg
496 501 def is_or_belongs_to?(arg)
497 502 if arg.is_a?(User)
498 503 self == arg
499 504 elsif arg.is_a?(Group)
500 505 arg.users.include?(self)
501 506 else
502 507 false
503 508 end
504 509 end
505 510
506 511 # Return true if the user is allowed to do the specified action on a specific context
507 512 # Action can be:
508 513 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
509 514 # * a permission Symbol (eg. :edit_project)
510 515 # Context can be:
511 516 # * a project : returns true if user is allowed to do the specified action on this project
512 517 # * an array of projects : returns true if user is allowed on every project
513 518 # * nil with options[:global] set : check if user has at least one role allowed for this action,
514 519 # or falls back to Non Member / Anonymous permissions depending if the user is logged
515 520 def allowed_to?(action, context, options={}, &block)
516 521 if context && context.is_a?(Project)
517 522 return false unless context.allows_to?(action)
518 523 # Admin users are authorized for anything else
519 524 return true if admin?
520 525
521 526 roles = roles_for_project(context)
522 527 return false unless roles
523 528 roles.any? {|role|
524 529 (context.is_public? || role.member?) &&
525 530 role.allowed_to?(action) &&
526 531 (block_given? ? yield(role, self) : true)
527 532 }
528 533 elsif context && context.is_a?(Array)
529 534 if context.empty?
530 535 false
531 536 else
532 537 # Authorize if user is authorized on every element of the array
533 538 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
534 539 end
535 540 elsif options[:global]
536 541 # Admin users are always authorized
537 542 return true if admin?
538 543
539 544 # authorize if user has at least one role that has this permission
540 545 roles = memberships.collect {|m| m.roles}.flatten.uniq
541 546 roles << (self.logged? ? Role.non_member : Role.anonymous)
542 547 roles.any? {|role|
543 548 role.allowed_to?(action) &&
544 549 (block_given? ? yield(role, self) : true)
545 550 }
546 551 else
547 552 false
548 553 end
549 554 end
550 555
551 556 # Is the user allowed to do the specified action on any project?
552 557 # See allowed_to? for the actions and valid options.
553 558 def allowed_to_globally?(action, options, &block)
554 559 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
555 560 end
556 561
557 562 # Returns true if the user is allowed to delete the user's own account
558 563 def own_account_deletable?
559 564 Setting.unsubscribe? &&
560 565 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
561 566 end
562 567
563 568 safe_attributes 'login',
564 569 'firstname',
565 570 'lastname',
566 571 'mail',
567 572 'mail_notification',
568 573 'notified_project_ids',
569 574 'language',
570 575 'custom_field_values',
571 576 'custom_fields',
572 577 'identity_url'
573 578
574 579 safe_attributes 'status',
575 580 'auth_source_id',
576 581 'generate_password',
577 582 'must_change_passwd',
578 583 :if => lambda {|user, current_user| current_user.admin?}
579 584
580 585 safe_attributes 'group_ids',
581 586 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
582 587
583 588 # Utility method to help check if a user should be notified about an
584 589 # event.
585 590 #
586 591 # TODO: only supports Issue events currently
587 592 def notify_about?(object)
588 593 if mail_notification == 'all'
589 594 true
590 595 elsif mail_notification.blank? || mail_notification == 'none'
591 596 false
592 597 else
593 598 case object
594 599 when Issue
595 600 case mail_notification
596 601 when 'selected', 'only_my_events'
597 602 # user receives notifications for created/assigned issues on unselected projects
598 603 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
599 604 when 'only_assigned'
600 605 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
601 606 when 'only_owner'
602 607 object.author == self
603 608 end
604 609 when News
605 610 # always send to project members except when mail_notification is set to 'none'
606 611 true
607 612 end
608 613 end
609 614 end
610 615
611 616 def self.current=(user)
612 617 Thread.current[:current_user] = user
613 618 end
614 619
615 620 def self.current
616 621 Thread.current[:current_user] ||= User.anonymous
617 622 end
618 623
619 624 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
620 625 # one anonymous user per database.
621 626 def self.anonymous
622 627 anonymous_user = AnonymousUser.first
623 628 if anonymous_user.nil?
624 629 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
625 630 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
626 631 end
627 632 anonymous_user
628 633 end
629 634
630 635 # Salts all existing unsalted passwords
631 636 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
632 637 # This method is used in the SaltPasswords migration and is to be kept as is
633 638 def self.salt_unsalted_passwords!
634 639 transaction do
635 640 User.where("salt IS NULL OR salt = ''").find_each do |user|
636 641 next if user.hashed_password.blank?
637 642 salt = User.generate_salt
638 643 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
639 644 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
640 645 end
641 646 end
642 647 end
643 648
644 649 protected
645 650
646 651 def validate_password_length
647 652 return if password.blank? && generate_password?
648 653 # Password length validation based on setting
649 654 if !password.nil? && password.size < Setting.password_min_length.to_i
650 655 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
651 656 end
652 657 end
653 658
654 659 private
655 660
656 661 def generate_password_if_needed
657 662 if generate_password? && auth_source.nil?
658 663 length = [Setting.password_min_length.to_i + 2, 10].max
659 664 random_password(length)
660 665 end
661 666 end
662 667
663 668 # Removes references that are not handled by associations
664 669 # Things that are not deleted are reassociated with the anonymous user
665 670 def remove_references_before_destroy
666 671 return if self.id.nil?
667 672
668 673 substitute = User.anonymous
669 674 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
670 675 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
671 676 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
672 677 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
673 678 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
674 679 JournalDetail.
675 680 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
676 681 update_all(['old_value = ?', substitute.id.to_s])
677 682 JournalDetail.
678 683 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
679 684 update_all(['value = ?', substitute.id.to_s])
680 685 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
681 686 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
682 687 # Remove private queries and keep public ones
683 688 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
684 689 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
685 690 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
686 691 Token.delete_all ['user_id = ?', id]
687 692 Watcher.delete_all ['user_id = ?', id]
688 693 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
689 694 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
690 695 end
691 696
692 697 # Return password digest
693 698 def self.hash_password(clear_password)
694 699 Digest::SHA1.hexdigest(clear_password || "")
695 700 end
696 701
697 702 # Returns a 128bits random salt as a hex string (32 chars long)
698 703 def self.generate_salt
699 704 Redmine::Utils.random_hex(16)
700 705 end
701 706
702 707 end
703 708
704 709 class AnonymousUser < User
705 710 validate :validate_anonymous_uniqueness, :on => :create
706 711
707 712 def validate_anonymous_uniqueness
708 713 # There should be only one AnonymousUser in the database
709 714 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
710 715 end
711 716
712 717 def available_custom_fields
713 718 []
714 719 end
715 720
716 721 # Overrides a few properties
717 722 def logged?; false end
718 723 def admin; false end
719 724 def name(*args); I18n.t(:label_user_anonymous) end
720 725 def mail; nil end
721 726 def time_zone; nil end
722 727 def rss_key; nil end
723 728
724 729 def pref
725 730 UserPreference.new(:user => self)
726 731 end
727 732
728 733 # Returns the user's bult-in role
729 734 def builtin_role
730 735 @builtin_role ||= Role.anonymous
731 736 end
732 737
733 738 def membership(*args)
734 739 nil
735 740 end
736 741
737 742 def member_of?(*args)
738 743 false
739 744 end
740 745
741 746 # Anonymous user can not be destroyed
742 747 def destroy
743 748 false
744 749 end
745 750 end
@@ -1,1149 +1,1154
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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
30 30 def setup
31 31 @admin = User.find(1)
32 32 @jsmith = User.find(2)
33 33 @dlopper = User.find(3)
34 34 end
35 35
36 36 def test_sorted_scope_should_sort_user_by_display_name
37 37 assert_equal User.all.map(&:name).map(&:downcase).sort,
38 38 User.sorted.all.map(&:name).map(&:downcase)
39 39 end
40 40
41 41 def test_generate
42 42 User.generate!(:firstname => 'Testing connection')
43 43 User.generate!(:firstname => 'Testing connection')
44 44 assert_equal 2, User.where(:firstname => 'Testing connection').count
45 45 end
46 46
47 47 def test_truth
48 48 assert_kind_of User, @jsmith
49 49 end
50 50
51 51 def test_mail_should_be_stripped
52 52 u = User.new
53 53 u.mail = " foo@bar.com "
54 54 assert_equal "foo@bar.com", u.mail
55 55 end
56 56
57 57 def test_mail_validation
58 58 u = User.new
59 59 u.mail = ''
60 60 assert !u.valid?
61 61 assert_include I18n.translate('activerecord.errors.messages.blank'), u.errors[:mail]
62 62 end
63 63
64 64 def test_login_length_validation
65 65 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
66 66 user.login = "x" * (User::LOGIN_LENGTH_LIMIT+1)
67 67 assert !user.valid?
68 68
69 69 user.login = "x" * (User::LOGIN_LENGTH_LIMIT)
70 70 assert user.valid?
71 71 assert user.save
72 72 end
73 73
74 74 def test_generate_password_should_respect_minimum_password_length
75 75 with_settings :password_min_length => 15 do
76 76 user = User.generate!(:generate_password => true)
77 77 assert user.password.length >= 15
78 78 end
79 79 end
80 80
81 81 def test_generate_password_should_not_generate_password_with_less_than_10_characters
82 82 with_settings :password_min_length => 4 do
83 83 user = User.generate!(:generate_password => true)
84 84 assert user.password.length >= 10
85 85 end
86 86 end
87 87
88 88 def test_generate_password_on_create_should_set_password
89 89 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
90 90 user.login = "newuser"
91 91 user.generate_password = true
92 92 assert user.save
93 93
94 94 password = user.password
95 95 assert user.check_password?(password)
96 96 end
97 97
98 98 def test_generate_password_on_update_should_update_password
99 99 user = User.find(2)
100 100 hash = user.hashed_password
101 101 user.generate_password = true
102 102 assert user.save
103 103
104 104 password = user.password
105 105 assert user.check_password?(password)
106 106 assert_not_equal hash, user.reload.hashed_password
107 107 end
108 108
109 109 def test_create
110 110 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
111 111
112 112 user.login = "jsmith"
113 113 user.password, user.password_confirmation = "password", "password"
114 114 # login uniqueness
115 115 assert !user.save
116 116 assert_equal 1, user.errors.count
117 117
118 118 user.login = "newuser"
119 119 user.password, user.password_confirmation = "password", "pass"
120 120 # password confirmation
121 121 assert !user.save
122 122 assert_equal 1, user.errors.count
123 123
124 124 user.password, user.password_confirmation = "password", "password"
125 125 assert user.save
126 126 end
127 127
128 128 def test_user_before_create_should_set_the_mail_notification_to_the_default_setting
129 129 @user1 = User.generate!
130 130 assert_equal 'only_my_events', @user1.mail_notification
131 131 with_settings :default_notification_option => 'all' do
132 132 @user2 = User.generate!
133 133 assert_equal 'all', @user2.mail_notification
134 134 end
135 135 end
136 136
137 137 def test_user_login_should_be_case_insensitive
138 138 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
139 139 u.login = 'newuser'
140 140 u.password, u.password_confirmation = "password", "password"
141 141 assert u.save
142 142 u = User.new(:firstname => "Similar", :lastname => "User",
143 143 :mail => "similaruser@somenet.foo")
144 144 u.login = 'NewUser'
145 145 u.password, u.password_confirmation = "password", "password"
146 146 assert !u.save
147 147 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:login]
148 148 end
149 149
150 150 def test_mail_uniqueness_should_not_be_case_sensitive
151 151 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
152 152 u.login = 'newuser1'
153 153 u.password, u.password_confirmation = "password", "password"
154 154 assert u.save
155 155
156 156 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
157 157 u.login = 'newuser2'
158 158 u.password, u.password_confirmation = "password", "password"
159 159 assert !u.save
160 160 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:mail]
161 161 end
162 162
163 163 def test_update
164 164 assert_equal "admin", @admin.login
165 165 @admin.login = "john"
166 166 assert @admin.save, @admin.errors.full_messages.join("; ")
167 167 @admin.reload
168 168 assert_equal "john", @admin.login
169 169 end
170 170
171 171 def test_update_should_not_fail_for_legacy_user_with_different_case_logins
172 172 u1 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser1@somenet.foo")
173 173 u1.login = 'newuser1'
174 174 assert u1.save
175 175
176 176 u2 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser2@somenet.foo")
177 177 u2.login = 'newuser1'
178 178 assert u2.save(:validate => false)
179 179
180 180 user = User.find(u2.id)
181 181 user.firstname = "firstname"
182 182 assert user.save, "Save failed"
183 183 end
184 184
185 185 def test_destroy_should_delete_members_and_roles
186 186 members = Member.where(:user_id => 2)
187 187 ms = members.count
188 188 rs = members.collect(&:roles).flatten.size
189 189 assert ms > 0
190 190 assert rs > 0
191 191 assert_difference 'Member.count', - ms do
192 192 assert_difference 'MemberRole.count', - rs do
193 193 User.find(2).destroy
194 194 end
195 195 end
196 196 assert_nil User.find_by_id(2)
197 197 assert_equal 0, Member.where(:user_id => 2).count
198 198 end
199 199
200 200 def test_destroy_should_update_attachments
201 201 attachment = Attachment.create!(:container => Project.find(1),
202 202 :file => uploaded_test_file("testfile.txt", "text/plain"),
203 203 :author_id => 2)
204 204
205 205 User.find(2).destroy
206 206 assert_nil User.find_by_id(2)
207 207 assert_equal User.anonymous, attachment.reload.author
208 208 end
209 209
210 210 def test_destroy_should_update_comments
211 211 comment = Comment.create!(
212 212 :commented => News.create!(:project_id => 1,
213 213 :author_id => 1, :title => 'foo', :description => 'foo'),
214 214 :author => User.find(2),
215 215 :comments => 'foo'
216 216 )
217 217
218 218 User.find(2).destroy
219 219 assert_nil User.find_by_id(2)
220 220 assert_equal User.anonymous, comment.reload.author
221 221 end
222 222
223 223 def test_destroy_should_update_issues
224 224 issue = Issue.create!(:project_id => 1, :author_id => 2,
225 225 :tracker_id => 1, :subject => 'foo')
226 226
227 227 User.find(2).destroy
228 228 assert_nil User.find_by_id(2)
229 229 assert_equal User.anonymous, issue.reload.author
230 230 end
231 231
232 232 def test_destroy_should_unassign_issues
233 233 issue = Issue.create!(:project_id => 1, :author_id => 1,
234 234 :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
235 235
236 236 User.find(2).destroy
237 237 assert_nil User.find_by_id(2)
238 238 assert_nil issue.reload.assigned_to
239 239 end
240 240
241 241 def test_destroy_should_update_journals
242 242 issue = Issue.create!(:project_id => 1, :author_id => 2,
243 243 :tracker_id => 1, :subject => 'foo')
244 244 issue.init_journal(User.find(2), "update")
245 245 issue.save!
246 246
247 247 User.find(2).destroy
248 248 assert_nil User.find_by_id(2)
249 249 assert_equal User.anonymous, issue.journals.first.reload.user
250 250 end
251 251
252 252 def test_destroy_should_update_journal_details_old_value
253 253 issue = Issue.create!(:project_id => 1, :author_id => 1,
254 254 :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
255 255 issue.init_journal(User.find(1), "update")
256 256 issue.assigned_to_id = nil
257 257 assert_difference 'JournalDetail.count' do
258 258 issue.save!
259 259 end
260 260 journal_detail = JournalDetail.order('id DESC').first
261 261 assert_equal '2', journal_detail.old_value
262 262
263 263 User.find(2).destroy
264 264 assert_nil User.find_by_id(2)
265 265 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
266 266 end
267 267
268 268 def test_destroy_should_update_journal_details_value
269 269 issue = Issue.create!(:project_id => 1, :author_id => 1,
270 270 :tracker_id => 1, :subject => 'foo')
271 271 issue.init_journal(User.find(1), "update")
272 272 issue.assigned_to_id = 2
273 273 assert_difference 'JournalDetail.count' do
274 274 issue.save!
275 275 end
276 276 journal_detail = JournalDetail.order('id DESC').first
277 277 assert_equal '2', journal_detail.value
278 278
279 279 User.find(2).destroy
280 280 assert_nil User.find_by_id(2)
281 281 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
282 282 end
283 283
284 284 def test_destroy_should_update_messages
285 285 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
286 286 message = Message.create!(:board_id => board.id, :author_id => 2,
287 287 :subject => 'foo', :content => 'foo')
288 288 User.find(2).destroy
289 289 assert_nil User.find_by_id(2)
290 290 assert_equal User.anonymous, message.reload.author
291 291 end
292 292
293 293 def test_destroy_should_update_news
294 294 news = News.create!(:project_id => 1, :author_id => 2,
295 295 :title => 'foo', :description => 'foo')
296 296 User.find(2).destroy
297 297 assert_nil User.find_by_id(2)
298 298 assert_equal User.anonymous, news.reload.author
299 299 end
300 300
301 301 def test_destroy_should_delete_private_queries
302 302 query = Query.new(:name => 'foo', :visibility => Query::VISIBILITY_PRIVATE)
303 303 query.project_id = 1
304 304 query.user_id = 2
305 305 query.save!
306 306
307 307 User.find(2).destroy
308 308 assert_nil User.find_by_id(2)
309 309 assert_nil Query.find_by_id(query.id)
310 310 end
311 311
312 312 def test_destroy_should_update_public_queries
313 313 query = Query.new(:name => 'foo', :visibility => Query::VISIBILITY_PUBLIC)
314 314 query.project_id = 1
315 315 query.user_id = 2
316 316 query.save!
317 317
318 318 User.find(2).destroy
319 319 assert_nil User.find_by_id(2)
320 320 assert_equal User.anonymous, query.reload.user
321 321 end
322 322
323 323 def test_destroy_should_update_time_entries
324 324 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today,
325 325 :activity => TimeEntryActivity.create!(:name => 'foo'))
326 326 entry.project_id = 1
327 327 entry.user_id = 2
328 328 entry.save!
329 329
330 330 User.find(2).destroy
331 331 assert_nil User.find_by_id(2)
332 332 assert_equal User.anonymous, entry.reload.user
333 333 end
334 334
335 335 def test_destroy_should_delete_tokens
336 336 token = Token.create!(:user_id => 2, :value => 'foo')
337 337
338 338 User.find(2).destroy
339 339 assert_nil User.find_by_id(2)
340 340 assert_nil Token.find_by_id(token.id)
341 341 end
342 342
343 343 def test_destroy_should_delete_watchers
344 344 issue = Issue.create!(:project_id => 1, :author_id => 1,
345 345 :tracker_id => 1, :subject => 'foo')
346 346 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
347 347
348 348 User.find(2).destroy
349 349 assert_nil User.find_by_id(2)
350 350 assert_nil Watcher.find_by_id(watcher.id)
351 351 end
352 352
353 353 def test_destroy_should_update_wiki_contents
354 354 wiki_content = WikiContent.create!(
355 355 :text => 'foo',
356 356 :author_id => 2,
357 357 :page => WikiPage.create!(:title => 'Foo',
358 358 :wiki => Wiki.create!(:project_id => 3,
359 359 :start_page => 'Start'))
360 360 )
361 361 wiki_content.text = 'bar'
362 362 assert_difference 'WikiContent::Version.count' do
363 363 wiki_content.save!
364 364 end
365 365
366 366 User.find(2).destroy
367 367 assert_nil User.find_by_id(2)
368 368 assert_equal User.anonymous, wiki_content.reload.author
369 369 wiki_content.versions.each do |version|
370 370 assert_equal User.anonymous, version.reload.author
371 371 end
372 372 end
373 373
374 374 def test_destroy_should_nullify_issue_categories
375 375 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
376 376
377 377 User.find(2).destroy
378 378 assert_nil User.find_by_id(2)
379 379 assert_nil category.reload.assigned_to_id
380 380 end
381 381
382 382 def test_destroy_should_nullify_changesets
383 383 changeset = Changeset.create!(
384 384 :repository => Repository::Subversion.create!(
385 385 :project_id => 1,
386 386 :url => 'file:///tmp',
387 387 :identifier => 'tmp'
388 388 ),
389 389 :revision => '12',
390 390 :committed_on => Time.now,
391 391 :committer => 'jsmith'
392 392 )
393 393 assert_equal 2, changeset.user_id
394 394
395 395 User.find(2).destroy
396 396 assert_nil User.find_by_id(2)
397 397 assert_nil changeset.reload.user_id
398 398 end
399 399
400 400 def test_anonymous_user_should_not_be_destroyable
401 401 assert_no_difference 'User.count' do
402 402 assert_equal false, User.anonymous.destroy
403 403 end
404 404 end
405 405
406 406 def test_validate_login_presence
407 407 @admin.login = ""
408 408 assert !@admin.save
409 409 assert_equal 1, @admin.errors.count
410 410 end
411 411
412 412 def test_validate_mail_notification_inclusion
413 413 u = User.new
414 414 u.mail_notification = 'foo'
415 415 u.save
416 416 assert_not_equal [], u.errors[:mail_notification]
417 417 end
418 418
419 419 def test_password
420 420 user = User.try_to_login("admin", "admin")
421 421 assert_kind_of User, user
422 422 assert_equal "admin", user.login
423 423 user.password = "hello123"
424 424 assert user.save
425 425
426 426 user = User.try_to_login("admin", "hello123")
427 427 assert_kind_of User, user
428 428 assert_equal "admin", user.login
429 429 end
430 430
431 431 def test_validate_password_length
432 432 with_settings :password_min_length => '100' do
433 433 user = User.new(:firstname => "new100",
434 434 :lastname => "user100", :mail => "newuser100@somenet.foo")
435 435 user.login = "newuser100"
436 436 user.password, user.password_confirmation = "password100", "password100"
437 437 assert !user.save
438 438 assert_equal 1, user.errors.count
439 439 end
440 440 end
441 441
442 442 def test_name_format
443 443 assert_equal 'John S.', @jsmith.name(:firstname_lastinitial)
444 444 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
445 assert_equal 'J. Smith', @jsmith.name(:firstinitial_lastname)
446 assert_equal 'J.-P. Lang', User.new(:firstname => 'Jean-Philippe', :lastname => 'Lang').name(:firstinitial_lastname)
447 end
448
449 def test_name_should_use_setting_as_default_format
445 450 with_settings :user_format => :firstname_lastname do
446 451 assert_equal 'John Smith', @jsmith.reload.name
447 452 end
448 453 with_settings :user_format => :username do
449 454 assert_equal 'jsmith', @jsmith.reload.name
450 455 end
451 456 with_settings :user_format => :lastname do
452 457 assert_equal 'Smith', @jsmith.reload.name
453 458 end
454 459 end
455 460
456 461 def test_today_should_return_the_day_according_to_user_time_zone
457 462 preference = User.find(1).pref
458 463 date = Date.new(2012, 05, 15)
459 464 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
460 465 Date.stubs(:today).returns(date)
461 466 Time.stubs(:now).returns(time)
462 467
463 468 preference.update_attribute :time_zone, 'Baku' # UTC+4
464 469 assert_equal '2012-05-16', User.find(1).today.to_s
465 470
466 471 preference.update_attribute :time_zone, 'La Paz' # UTC-4
467 472 assert_equal '2012-05-15', User.find(1).today.to_s
468 473
469 474 preference.update_attribute :time_zone, ''
470 475 assert_equal '2012-05-15', User.find(1).today.to_s
471 476 end
472 477
473 478 def test_time_to_date_should_return_the_date_according_to_user_time_zone
474 479 preference = User.find(1).pref
475 480 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
476 481
477 482 preference.update_attribute :time_zone, 'Baku' # UTC+4
478 483 assert_equal '2012-05-16', User.find(1).time_to_date(time).to_s
479 484
480 485 preference.update_attribute :time_zone, 'La Paz' # UTC-4
481 486 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
482 487
483 488 preference.update_attribute :time_zone, ''
484 489 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
485 490 end
486 491
487 492 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
488 493 with_settings :user_format => 'lastname_coma_firstname' do
489 494 assert_equal ['users.lastname', 'users.firstname', 'users.id'],
490 495 User.fields_for_order_statement
491 496 end
492 497 end
493 498
494 499 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
495 500 with_settings :user_format => 'lastname_firstname' do
496 501 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'],
497 502 User.fields_for_order_statement('authors')
498 503 end
499 504 end
500 505
501 506 def test_fields_for_order_statement_with_blank_format_should_return_default
502 507 with_settings :user_format => '' do
503 508 assert_equal ['users.firstname', 'users.lastname', 'users.id'],
504 509 User.fields_for_order_statement
505 510 end
506 511 end
507 512
508 513 def test_fields_for_order_statement_with_invalid_format_should_return_default
509 514 with_settings :user_format => 'foo' do
510 515 assert_equal ['users.firstname', 'users.lastname', 'users.id'],
511 516 User.fields_for_order_statement
512 517 end
513 518 end
514 519
515 520 test ".try_to_login with good credentials should return the user" do
516 521 user = User.try_to_login("admin", "admin")
517 522 assert_kind_of User, user
518 523 assert_equal "admin", user.login
519 524 end
520 525
521 526 test ".try_to_login with wrong credentials should return nil" do
522 527 assert_nil User.try_to_login("admin", "foo")
523 528 end
524 529
525 530 def test_try_to_login_with_locked_user_should_return_nil
526 531 @jsmith.status = User::STATUS_LOCKED
527 532 @jsmith.save!
528 533
529 534 user = User.try_to_login("jsmith", "jsmith")
530 535 assert_equal nil, user
531 536 end
532 537
533 538 def test_try_to_login_with_locked_user_and_not_active_only_should_return_user
534 539 @jsmith.status = User::STATUS_LOCKED
535 540 @jsmith.save!
536 541
537 542 user = User.try_to_login("jsmith", "jsmith", false)
538 543 assert_equal @jsmith, user
539 544 end
540 545
541 546 test ".try_to_login should fall-back to case-insensitive if user login is not found as-typed" do
542 547 user = User.try_to_login("AdMin", "admin")
543 548 assert_kind_of User, user
544 549 assert_equal "admin", user.login
545 550 end
546 551
547 552 test ".try_to_login should select the exact matching user first" do
548 553 case_sensitive_user = User.generate! do |user|
549 554 user.password = "admin123"
550 555 end
551 556 # bypass validations to make it appear like existing data
552 557 case_sensitive_user.update_attribute(:login, 'ADMIN')
553 558
554 559 user = User.try_to_login("ADMIN", "admin123")
555 560 assert_kind_of User, user
556 561 assert_equal "ADMIN", user.login
557 562 end
558 563
559 564 if ldap_configured?
560 565 context "#try_to_login using LDAP" do
561 566 context "with failed connection to the LDAP server" do
562 567 should "return nil" do
563 568 @auth_source = AuthSourceLdap.find(1)
564 569 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
565 570
566 571 assert_equal nil, User.try_to_login('edavis', 'wrong')
567 572 end
568 573 end
569 574
570 575 context "with an unsuccessful authentication" do
571 576 should "return nil" do
572 577 assert_equal nil, User.try_to_login('edavis', 'wrong')
573 578 end
574 579 end
575 580
576 581 context "binding with user's account" do
577 582 setup do
578 583 @auth_source = AuthSourceLdap.find(1)
579 584 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
580 585 @auth_source.account_password = ''
581 586 @auth_source.save!
582 587
583 588 @ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
584 589 @ldap_user.login = 'example1'
585 590 @ldap_user.save!
586 591 end
587 592
588 593 context "with a successful authentication" do
589 594 should "return the user" do
590 595 assert_equal @ldap_user, User.try_to_login('example1', '123456')
591 596 end
592 597 end
593 598
594 599 context "with an unsuccessful authentication" do
595 600 should "return nil" do
596 601 assert_nil User.try_to_login('example1', '11111')
597 602 end
598 603 end
599 604 end
600 605
601 606 context "on the fly registration" do
602 607 setup do
603 608 @auth_source = AuthSourceLdap.find(1)
604 609 @auth_source.update_attribute :onthefly_register, true
605 610 end
606 611
607 612 context "with a successful authentication" do
608 613 should "create a new user account if it doesn't exist" do
609 614 assert_difference('User.count') do
610 615 user = User.try_to_login('edavis', '123456')
611 616 assert !user.admin?
612 617 end
613 618 end
614 619
615 620 should "retrieve existing user" do
616 621 user = User.try_to_login('edavis', '123456')
617 622 user.admin = true
618 623 user.save!
619 624
620 625 assert_no_difference('User.count') do
621 626 user = User.try_to_login('edavis', '123456')
622 627 assert user.admin?
623 628 end
624 629 end
625 630 end
626 631
627 632 context "binding with user's account" do
628 633 setup do
629 634 @auth_source = AuthSourceLdap.find(1)
630 635 @auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
631 636 @auth_source.account_password = ''
632 637 @auth_source.save!
633 638 end
634 639
635 640 context "with a successful authentication" do
636 641 should "create a new user account if it doesn't exist" do
637 642 assert_difference('User.count') do
638 643 user = User.try_to_login('example1', '123456')
639 644 assert_kind_of User, user
640 645 end
641 646 end
642 647 end
643 648
644 649 context "with an unsuccessful authentication" do
645 650 should "return nil" do
646 651 assert_nil User.try_to_login('example1', '11111')
647 652 end
648 653 end
649 654 end
650 655 end
651 656 end
652 657
653 658 else
654 659 puts "Skipping LDAP tests."
655 660 end
656 661
657 662 def test_create_anonymous
658 663 AnonymousUser.delete_all
659 664 anon = User.anonymous
660 665 assert !anon.new_record?
661 666 assert_kind_of AnonymousUser, anon
662 667 end
663 668
664 669 def test_ensure_single_anonymous_user
665 670 AnonymousUser.delete_all
666 671 anon1 = User.anonymous
667 672 assert !anon1.new_record?
668 673 assert_kind_of AnonymousUser, anon1
669 674 anon2 = AnonymousUser.create(
670 675 :lastname => 'Anonymous', :firstname => '',
671 676 :mail => '', :login => '', :status => 0)
672 677 assert_equal 1, anon2.errors.count
673 678 end
674 679
675 680 def test_rss_key
676 681 assert_nil @jsmith.rss_token
677 682 key = @jsmith.rss_key
678 683 assert_equal 40, key.length
679 684
680 685 @jsmith.reload
681 686 assert_equal key, @jsmith.rss_key
682 687 end
683 688
684 689 def test_rss_key_should_not_be_generated_twice
685 690 assert_difference 'Token.count', 1 do
686 691 key1 = @jsmith.rss_key
687 692 key2 = @jsmith.rss_key
688 693 assert_equal key1, key2
689 694 end
690 695 end
691 696
692 697 def test_api_key_should_not_be_generated_twice
693 698 assert_difference 'Token.count', 1 do
694 699 key1 = @jsmith.api_key
695 700 key2 = @jsmith.api_key
696 701 assert_equal key1, key2
697 702 end
698 703 end
699 704
700 705 test "#api_key should generate a new one if the user doesn't have one" do
701 706 user = User.generate!(:api_token => nil)
702 707 assert_nil user.api_token
703 708
704 709 key = user.api_key
705 710 assert_equal 40, key.length
706 711 user.reload
707 712 assert_equal key, user.api_key
708 713 end
709 714
710 715 test "#api_key should return the existing api token value" do
711 716 user = User.generate!
712 717 token = Token.create!(:action => 'api')
713 718 user.api_token = token
714 719 assert user.save
715 720
716 721 assert_equal token.value, user.api_key
717 722 end
718 723
719 724 test "#find_by_api_key should return nil if no matching key is found" do
720 725 assert_nil User.find_by_api_key('zzzzzzzzz')
721 726 end
722 727
723 728 test "#find_by_api_key should return nil if the key is found for an inactive user" do
724 729 user = User.generate!
725 730 user.status = User::STATUS_LOCKED
726 731 token = Token.create!(:action => 'api')
727 732 user.api_token = token
728 733 user.save
729 734
730 735 assert_nil User.find_by_api_key(token.value)
731 736 end
732 737
733 738 test "#find_by_api_key should return the user if the key is found for an active user" do
734 739 user = User.generate!
735 740 token = Token.create!(:action => 'api')
736 741 user.api_token = token
737 742 user.save
738 743
739 744 assert_equal user, User.find_by_api_key(token.value)
740 745 end
741 746
742 747 def test_default_admin_account_changed_should_return_false_if_account_was_not_changed
743 748 user = User.find_by_login("admin")
744 749 user.password = "admin"
745 750 assert user.save(:validate => false)
746 751
747 752 assert_equal false, User.default_admin_account_changed?
748 753 end
749 754
750 755 def test_default_admin_account_changed_should_return_true_if_password_was_changed
751 756 user = User.find_by_login("admin")
752 757 user.password = "newpassword"
753 758 user.save!
754 759
755 760 assert_equal true, User.default_admin_account_changed?
756 761 end
757 762
758 763 def test_default_admin_account_changed_should_return_true_if_account_is_disabled
759 764 user = User.find_by_login("admin")
760 765 user.password = "admin"
761 766 user.status = User::STATUS_LOCKED
762 767 assert user.save(:validate => false)
763 768
764 769 assert_equal true, User.default_admin_account_changed?
765 770 end
766 771
767 772 def test_default_admin_account_changed_should_return_true_if_account_does_not_exist
768 773 user = User.find_by_login("admin")
769 774 user.destroy
770 775
771 776 assert_equal true, User.default_admin_account_changed?
772 777 end
773 778
774 779 def test_membership_with_project_should_return_membership
775 780 project = Project.find(1)
776 781
777 782 membership = @jsmith.membership(project)
778 783 assert_kind_of Member, membership
779 784 assert_equal @jsmith, membership.user
780 785 assert_equal project, membership.project
781 786 end
782 787
783 788 def test_membership_with_project_id_should_return_membership
784 789 project = Project.find(1)
785 790
786 791 membership = @jsmith.membership(1)
787 792 assert_kind_of Member, membership
788 793 assert_equal @jsmith, membership.user
789 794 assert_equal project, membership.project
790 795 end
791 796
792 797 def test_membership_for_non_member_should_return_nil
793 798 project = Project.find(1)
794 799
795 800 user = User.generate!
796 801 membership = user.membership(1)
797 802 assert_nil membership
798 803 end
799 804
800 805 def test_roles_for_project
801 806 # user with a role
802 807 roles = @jsmith.roles_for_project(Project.find(1))
803 808 assert_kind_of Role, roles.first
804 809 assert_equal "Manager", roles.first.name
805 810
806 811 # user with no role
807 812 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
808 813 end
809 814
810 815 def test_projects_by_role_for_user_with_role
811 816 user = User.find(2)
812 817 assert_kind_of Hash, user.projects_by_role
813 818 assert_equal 2, user.projects_by_role.size
814 819 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
815 820 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
816 821 end
817 822
818 823 def test_accessing_projects_by_role_with_no_projects_should_return_an_empty_array
819 824 user = User.find(2)
820 825 assert_equal [], user.projects_by_role[Role.find(3)]
821 826 # should not update the hash
822 827 assert_nil user.projects_by_role.values.detect(&:blank?)
823 828 end
824 829
825 830 def test_projects_by_role_for_user_with_no_role
826 831 user = User.generate!
827 832 assert_equal({}, user.projects_by_role)
828 833 end
829 834
830 835 def test_projects_by_role_for_anonymous
831 836 assert_equal({}, User.anonymous.projects_by_role)
832 837 end
833 838
834 839 def test_valid_notification_options
835 840 # without memberships
836 841 assert_equal 5, User.find(7).valid_notification_options.size
837 842 # with memberships
838 843 assert_equal 6, User.find(2).valid_notification_options.size
839 844 end
840 845
841 846 def test_valid_notification_options_class_method
842 847 assert_equal 5, User.valid_notification_options.size
843 848 assert_equal 5, User.valid_notification_options(User.find(7)).size
844 849 assert_equal 6, User.valid_notification_options(User.find(2)).size
845 850 end
846 851
847 852 def test_mail_notification_all
848 853 @jsmith.mail_notification = 'all'
849 854 @jsmith.notified_project_ids = []
850 855 @jsmith.save
851 856 @jsmith.reload
852 857 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
853 858 end
854 859
855 860 def test_mail_notification_selected
856 861 @jsmith.mail_notification = 'selected'
857 862 @jsmith.notified_project_ids = [1]
858 863 @jsmith.save
859 864 @jsmith.reload
860 865 assert Project.find(1).recipients.include?(@jsmith.mail)
861 866 end
862 867
863 868 def test_mail_notification_only_my_events
864 869 @jsmith.mail_notification = 'only_my_events'
865 870 @jsmith.notified_project_ids = []
866 871 @jsmith.save
867 872 @jsmith.reload
868 873 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
869 874 end
870 875
871 876 def test_comments_sorting_preference
872 877 assert !@jsmith.wants_comments_in_reverse_order?
873 878 @jsmith.pref.comments_sorting = 'asc'
874 879 assert !@jsmith.wants_comments_in_reverse_order?
875 880 @jsmith.pref.comments_sorting = 'desc'
876 881 assert @jsmith.wants_comments_in_reverse_order?
877 882 end
878 883
879 884 def test_find_by_mail_should_be_case_insensitive
880 885 u = User.find_by_mail('JSmith@somenet.foo')
881 886 assert_not_nil u
882 887 assert_equal 'jsmith@somenet.foo', u.mail
883 888 end
884 889
885 890 def test_random_password
886 891 u = User.new
887 892 u.random_password
888 893 assert !u.password.blank?
889 894 assert !u.password_confirmation.blank?
890 895 end
891 896
892 897 test "#change_password_allowed? should be allowed if no auth source is set" do
893 898 user = User.generate!
894 899 assert user.change_password_allowed?
895 900 end
896 901
897 902 test "#change_password_allowed? should delegate to the auth source" do
898 903 user = User.generate!
899 904
900 905 allowed_auth_source = AuthSource.generate!
901 906 def allowed_auth_source.allow_password_changes?; true; end
902 907
903 908 denied_auth_source = AuthSource.generate!
904 909 def denied_auth_source.allow_password_changes?; false; end
905 910
906 911 assert user.change_password_allowed?
907 912
908 913 user.auth_source = allowed_auth_source
909 914 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
910 915
911 916 user.auth_source = denied_auth_source
912 917 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
913 918 end
914 919
915 920 def test_own_account_deletable_should_be_true_with_unsubscrive_enabled
916 921 with_settings :unsubscribe => '1' do
917 922 assert_equal true, User.find(2).own_account_deletable?
918 923 end
919 924 end
920 925
921 926 def test_own_account_deletable_should_be_false_with_unsubscrive_disabled
922 927 with_settings :unsubscribe => '0' do
923 928 assert_equal false, User.find(2).own_account_deletable?
924 929 end
925 930 end
926 931
927 932 def test_own_account_deletable_should_be_false_for_a_single_admin
928 933 User.delete_all(["admin = ? AND id <> ?", true, 1])
929 934
930 935 with_settings :unsubscribe => '1' do
931 936 assert_equal false, User.find(1).own_account_deletable?
932 937 end
933 938 end
934 939
935 940 def test_own_account_deletable_should_be_true_for_an_admin_if_other_admin_exists
936 941 User.generate! do |user|
937 942 user.admin = true
938 943 end
939 944
940 945 with_settings :unsubscribe => '1' do
941 946 assert_equal true, User.find(1).own_account_deletable?
942 947 end
943 948 end
944 949
945 950 context "#allowed_to?" do
946 951 context "with a unique project" do
947 952 should "return false if project is archived" do
948 953 project = Project.find(1)
949 954 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
950 955 assert_equal false, @admin.allowed_to?(:view_issues, Project.find(1))
951 956 end
952 957
953 958 should "return false for write action if project is closed" do
954 959 project = Project.find(1)
955 960 Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
956 961 assert_equal false, @admin.allowed_to?(:edit_project, Project.find(1))
957 962 end
958 963
959 964 should "return true for read action if project is closed" do
960 965 project = Project.find(1)
961 966 Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
962 967 assert_equal true, @admin.allowed_to?(:view_project, Project.find(1))
963 968 end
964 969
965 970 should "return false if related module is disabled" do
966 971 project = Project.find(1)
967 972 project.enabled_module_names = ["issue_tracking"]
968 973 assert_equal true, @admin.allowed_to?(:add_issues, project)
969 974 assert_equal false, @admin.allowed_to?(:view_wiki_pages, project)
970 975 end
971 976
972 977 should "authorize nearly everything for admin users" do
973 978 project = Project.find(1)
974 979 assert ! @admin.member_of?(project)
975 980 %w(edit_issues delete_issues manage_news add_documents manage_wiki).each do |p|
976 981 assert_equal true, @admin.allowed_to?(p.to_sym, project)
977 982 end
978 983 end
979 984
980 985 should "authorize normal users depending on their roles" do
981 986 project = Project.find(1)
982 987 assert_equal true, @jsmith.allowed_to?(:delete_messages, project) #Manager
983 988 assert_equal false, @dlopper.allowed_to?(:delete_messages, project) #Developper
984 989 end
985 990 end
986 991
987 992 context "with multiple projects" do
988 993 should "return false if array is empty" do
989 994 assert_equal false, @admin.allowed_to?(:view_project, [])
990 995 end
991 996
992 997 should "return true only if user has permission on all these projects" do
993 998 assert_equal true, @admin.allowed_to?(:view_project, Project.all)
994 999 assert_equal false, @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
995 1000 assert_equal true, @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
996 1001 assert_equal false, @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
997 1002 end
998 1003
999 1004 should "behave correctly with arrays of 1 project" do
1000 1005 assert_equal false, User.anonymous.allowed_to?(:delete_issues, [Project.first])
1001 1006 end
1002 1007 end
1003 1008
1004 1009 context "with options[:global]" do
1005 1010 should "authorize if user has at least one role that has this permission" do
1006 1011 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
1007 1012 @anonymous = User.find(6)
1008 1013 assert_equal true, @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
1009 1014 assert_equal false, @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
1010 1015 assert_equal true, @dlopper2.allowed_to?(:add_issues, nil, :global => true)
1011 1016 assert_equal false, @anonymous.allowed_to?(:add_issues, nil, :global => true)
1012 1017 assert_equal true, @anonymous.allowed_to?(:view_issues, nil, :global => true)
1013 1018 end
1014 1019 end
1015 1020 end
1016 1021
1017 1022 context "User#notify_about?" do
1018 1023 context "Issues" do
1019 1024 setup do
1020 1025 @project = Project.find(1)
1021 1026 @author = User.generate!
1022 1027 @assignee = User.generate!
1023 1028 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
1024 1029 end
1025 1030
1026 1031 should "be true for a user with :all" do
1027 1032 @author.update_attribute(:mail_notification, 'all')
1028 1033 assert @author.notify_about?(@issue)
1029 1034 end
1030 1035
1031 1036 should "be false for a user with :none" do
1032 1037 @author.update_attribute(:mail_notification, 'none')
1033 1038 assert ! @author.notify_about?(@issue)
1034 1039 end
1035 1040
1036 1041 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
1037 1042 @user = User.generate!(:mail_notification => 'only_my_events')
1038 1043 Member.create!(:user => @user, :project => @project, :role_ids => [1])
1039 1044 assert ! @user.notify_about?(@issue)
1040 1045 end
1041 1046
1042 1047 should "be true for a user with :only_my_events and is the author" do
1043 1048 @author.update_attribute(:mail_notification, 'only_my_events')
1044 1049 assert @author.notify_about?(@issue)
1045 1050 end
1046 1051
1047 1052 should "be true for a user with :only_my_events and is the assignee" do
1048 1053 @assignee.update_attribute(:mail_notification, 'only_my_events')
1049 1054 assert @assignee.notify_about?(@issue)
1050 1055 end
1051 1056
1052 1057 should "be true for a user with :only_assigned and is the assignee" do
1053 1058 @assignee.update_attribute(:mail_notification, 'only_assigned')
1054 1059 assert @assignee.notify_about?(@issue)
1055 1060 end
1056 1061
1057 1062 should "be false for a user with :only_assigned and is not the assignee" do
1058 1063 @author.update_attribute(:mail_notification, 'only_assigned')
1059 1064 assert ! @author.notify_about?(@issue)
1060 1065 end
1061 1066
1062 1067 should "be true for a user with :only_owner and is the author" do
1063 1068 @author.update_attribute(:mail_notification, 'only_owner')
1064 1069 assert @author.notify_about?(@issue)
1065 1070 end
1066 1071
1067 1072 should "be false for a user with :only_owner and is not the author" do
1068 1073 @assignee.update_attribute(:mail_notification, 'only_owner')
1069 1074 assert ! @assignee.notify_about?(@issue)
1070 1075 end
1071 1076
1072 1077 should "be true for a user with :selected and is the author" do
1073 1078 @author.update_attribute(:mail_notification, 'selected')
1074 1079 assert @author.notify_about?(@issue)
1075 1080 end
1076 1081
1077 1082 should "be true for a user with :selected and is the assignee" do
1078 1083 @assignee.update_attribute(:mail_notification, 'selected')
1079 1084 assert @assignee.notify_about?(@issue)
1080 1085 end
1081 1086
1082 1087 should "be false for a user with :selected and is not the author or assignee" do
1083 1088 @user = User.generate!(:mail_notification => 'selected')
1084 1089 Member.create!(:user => @user, :project => @project, :role_ids => [1])
1085 1090 assert ! @user.notify_about?(@issue)
1086 1091 end
1087 1092 end
1088 1093 end
1089 1094
1090 1095 def test_notify_about_news
1091 1096 user = User.generate!
1092 1097 news = News.new
1093 1098
1094 1099 User::MAIL_NOTIFICATION_OPTIONS.map(&:first).each do |option|
1095 1100 user.mail_notification = option
1096 1101 assert_equal (option != 'none'), user.notify_about?(news)
1097 1102 end
1098 1103 end
1099 1104
1100 1105 def test_salt_unsalted_passwords
1101 1106 # Restore a user with an unsalted password
1102 1107 user = User.find(1)
1103 1108 user.salt = nil
1104 1109 user.hashed_password = User.hash_password("unsalted")
1105 1110 user.save!
1106 1111
1107 1112 User.salt_unsalted_passwords!
1108 1113
1109 1114 user.reload
1110 1115 # Salt added
1111 1116 assert !user.salt.blank?
1112 1117 # Password still valid
1113 1118 assert user.check_password?("unsalted")
1114 1119 assert_equal user, User.try_to_login(user.login, "unsalted")
1115 1120 end
1116 1121
1117 1122 if Object.const_defined?(:OpenID)
1118 1123 def test_setting_identity_url
1119 1124 normalized_open_id_url = 'http://example.com/'
1120 1125 u = User.new( :identity_url => 'http://example.com/' )
1121 1126 assert_equal normalized_open_id_url, u.identity_url
1122 1127 end
1123 1128
1124 1129 def test_setting_identity_url_without_trailing_slash
1125 1130 normalized_open_id_url = 'http://example.com/'
1126 1131 u = User.new( :identity_url => 'http://example.com' )
1127 1132 assert_equal normalized_open_id_url, u.identity_url
1128 1133 end
1129 1134
1130 1135 def test_setting_identity_url_without_protocol
1131 1136 normalized_open_id_url = 'http://example.com/'
1132 1137 u = User.new( :identity_url => 'example.com' )
1133 1138 assert_equal normalized_open_id_url, u.identity_url
1134 1139 end
1135 1140
1136 1141 def test_setting_blank_identity_url
1137 1142 u = User.new( :identity_url => 'example.com' )
1138 1143 u.identity_url = ''
1139 1144 assert u.identity_url.blank?
1140 1145 end
1141 1146
1142 1147 def test_setting_invalid_identity_url
1143 1148 u = User.new( :identity_url => 'this is not an openid url' )
1144 1149 assert u.identity_url.blank?
1145 1150 end
1146 1151 else
1147 1152 puts "Skipping openid tests."
1148 1153 end
1149 1154 end
General Comments 0
You need to be logged in to leave comments. Login now