##// END OF EJS Templates
Merged r4727 and r4730 from trunk....
Jean-Philippe Lang -
r4651:27a319e66d43
parent child
Show More
@@ -1,507 +1,511
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 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 29 USER_FORMATS = {
30 30 :firstname_lastname => '#{firstname} #{lastname}',
31 31 :firstname => '#{firstname}',
32 32 :lastname_firstname => '#{lastname} #{firstname}',
33 33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 34 :username => '#{login}'
35 35 }
36 36
37 37 MAIL_NOTIFICATION_OPTIONS = [
38 38 ['all', :label_user_mail_option_all],
39 39 ['selected', :label_user_mail_option_selected],
40 40 ['only_my_events', :label_user_mail_option_only_my_events],
41 41 ['only_assigned', :label_user_mail_option_only_assigned],
42 42 ['only_owner', :label_user_mail_option_only_owner],
43 43 ['none', :label_user_mail_option_none]
44 44 ]
45 45
46 46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
49 49 has_many :changesets, :dependent => :nullify
50 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 51 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
52 52 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
53 53 belongs_to :auth_source
54 54
55 55 # Active non-anonymous users scope
56 56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57 57
58 58 acts_as_customizable
59 59
60 60 attr_accessor :password, :password_confirmation
61 61 attr_accessor :last_before_login_on
62 62 # Prevents unauthorized assignments
63 63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
64 64
65 65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
66 66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
67 67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
68 68 # Login must contain lettres, numbers, underscores only
69 69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
70 70 validates_length_of :login, :maximum => 30
71 71 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
72 72 validates_length_of :firstname, :lastname, :maximum => 30
73 73 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
74 74 validates_length_of :mail, :maximum => 60, :allow_nil => true
75 75 validates_confirmation_of :password, :allow_nil => true
76 76 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
77 77
78 78 def before_create
79 79 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
80 80 true
81 81 end
82 82
83 83 def before_save
84 84 # update hashed_password if password was set
85 85 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
86 86 end
87 87
88 88 def reload(*args)
89 89 @name = nil
90 90 super
91 91 end
92 92
93 93 def mail=(arg)
94 94 write_attribute(:mail, arg.to_s.strip)
95 95 end
96 96
97 97 def identity_url=(url)
98 98 if url.blank?
99 99 write_attribute(:identity_url, '')
100 100 else
101 101 begin
102 102 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
103 103 rescue OpenIdAuthentication::InvalidOpenId
104 104 # Invlaid url, don't save
105 105 end
106 106 end
107 107 self.read_attribute(:identity_url)
108 108 end
109 109
110 110 # Returns the user that matches provided login and password, or nil
111 111 def self.try_to_login(login, password)
112 112 # Make sure no one can sign in with an empty password
113 113 return nil if password.to_s.empty?
114 114 user = find_by_login(login)
115 115 if user
116 116 # user is already in local database
117 117 return nil if !user.active?
118 118 if user.auth_source
119 119 # user has an external authentication method
120 120 return nil unless user.auth_source.authenticate(login, password)
121 121 else
122 122 # authentication with local password
123 123 return nil unless User.hash_password(password) == user.hashed_password
124 124 end
125 125 else
126 126 # user is not yet registered, try to authenticate with available sources
127 127 attrs = AuthSource.authenticate(login, password)
128 128 if attrs
129 129 user = new(attrs)
130 130 user.login = login
131 131 user.language = Setting.default_language
132 132 if user.save
133 133 user.reload
134 134 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
135 135 end
136 136 end
137 137 end
138 138 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
139 139 user
140 140 rescue => text
141 141 raise text
142 142 end
143 143
144 144 # Returns the user who matches the given autologin +key+ or nil
145 145 def self.try_to_autologin(key)
146 146 tokens = Token.find_all_by_action_and_value('autologin', key)
147 147 # Make sure there's only 1 token that matches the key
148 148 if tokens.size == 1
149 149 token = tokens.first
150 150 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
151 151 token.user.update_attribute(:last_login_on, Time.now)
152 152 token.user
153 153 end
154 154 end
155 155 end
156 156
157 157 # Return user's full name for display
158 158 def name(formatter = nil)
159 159 if formatter
160 160 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
161 161 else
162 162 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
163 163 end
164 164 end
165 165
166 166 def active?
167 167 self.status == STATUS_ACTIVE
168 168 end
169 169
170 170 def registered?
171 171 self.status == STATUS_REGISTERED
172 172 end
173 173
174 174 def locked?
175 175 self.status == STATUS_LOCKED
176 176 end
177 177
178 178 def activate
179 179 self.status = STATUS_ACTIVE
180 180 end
181 181
182 182 def register
183 183 self.status = STATUS_REGISTERED
184 184 end
185 185
186 186 def lock
187 187 self.status = STATUS_LOCKED
188 188 end
189 189
190 190 def activate!
191 191 update_attribute(:status, STATUS_ACTIVE)
192 192 end
193 193
194 194 def register!
195 195 update_attribute(:status, STATUS_REGISTERED)
196 196 end
197 197
198 198 def lock!
199 199 update_attribute(:status, STATUS_LOCKED)
200 200 end
201 201
202 202 def check_password?(clear_password)
203 203 if auth_source_id.present?
204 204 auth_source.authenticate(self.login, clear_password)
205 205 else
206 206 User.hash_password(clear_password) == self.hashed_password
207 207 end
208 208 end
209 209
210 210 # Does the backend storage allow this user to change their password?
211 211 def change_password_allowed?
212 212 return true if auth_source_id.blank?
213 213 return auth_source.allow_password_changes?
214 214 end
215 215
216 216 # Generate and set a random password. Useful for automated user creation
217 217 # Based on Token#generate_token_value
218 218 #
219 219 def random_password
220 220 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
221 221 password = ''
222 222 40.times { |i| password << chars[rand(chars.size-1)] }
223 223 self.password = password
224 224 self.password_confirmation = password
225 225 self
226 226 end
227 227
228 228 def pref
229 229 self.preference ||= UserPreference.new(:user => self)
230 230 end
231 231
232 232 def time_zone
233 233 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
234 234 end
235 235
236 236 def wants_comments_in_reverse_order?
237 237 self.pref[:comments_sorting] == 'desc'
238 238 end
239 239
240 240 # Return user's RSS key (a 40 chars long string), used to access feeds
241 241 def rss_key
242 242 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
243 243 token.value
244 244 end
245 245
246 246 # Return user's API key (a 40 chars long string), used to access the API
247 247 def api_key
248 248 token = self.api_token || self.create_api_token(:action => 'api')
249 249 token.value
250 250 end
251 251
252 252 # Return an array of project ids for which the user has explicitly turned mail notifications on
253 253 def notified_projects_ids
254 254 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
255 255 end
256 256
257 257 def notified_project_ids=(ids)
258 258 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
259 259 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
260 260 @notified_projects_ids = nil
261 261 notified_projects_ids
262 262 end
263 263
264 # Only users that belong to more than 1 project can select projects for which they are notified
265 264 def valid_notification_options
265 self.class.valid_notification_options(self)
266 end
267
268 # Only users that belong to more than 1 project can select projects for which they are notified
269 def self.valid_notification_options(user=nil)
266 270 # Note that @user.membership.size would fail since AR ignores
267 271 # :include association option when doing a count
268 if memberships.length < 1
269 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == 'selected'}
272 if user.nil? || user.memberships.length < 1
273 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
270 274 else
271 275 MAIL_NOTIFICATION_OPTIONS
272 276 end
273 277 end
274 278
275 279 # Find a user account by matching the exact login and then a case-insensitive
276 280 # version. Exact matches will be given priority.
277 281 def self.find_by_login(login)
278 282 # force string comparison to be case sensitive on MySQL
279 283 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
280 284
281 285 # First look for an exact match
282 286 user = first(:conditions => ["#{type_cast} login = ?", login])
283 287 # Fail over to case-insensitive if none was found
284 288 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
285 289 end
286 290
287 291 def self.find_by_rss_key(key)
288 292 token = Token.find_by_value(key)
289 293 token && token.user.active? ? token.user : nil
290 294 end
291 295
292 296 def self.find_by_api_key(key)
293 297 token = Token.find_by_action_and_value('api', key)
294 298 token && token.user.active? ? token.user : nil
295 299 end
296 300
297 301 # Makes find_by_mail case-insensitive
298 302 def self.find_by_mail(mail)
299 303 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
300 304 end
301 305
302 306 def to_s
303 307 name
304 308 end
305 309
306 310 # Returns the current day according to user's time zone
307 311 def today
308 312 if time_zone.nil?
309 313 Date.today
310 314 else
311 315 Time.now.in_time_zone(time_zone).to_date
312 316 end
313 317 end
314 318
315 319 def logged?
316 320 true
317 321 end
318 322
319 323 def anonymous?
320 324 !logged?
321 325 end
322 326
323 327 # Return user's roles for project
324 328 def roles_for_project(project)
325 329 roles = []
326 330 # No role on archived projects
327 331 return roles unless project && project.active?
328 332 if logged?
329 333 # Find project membership
330 334 membership = memberships.detect {|m| m.project_id == project.id}
331 335 if membership
332 336 roles = membership.roles
333 337 else
334 338 @role_non_member ||= Role.non_member
335 339 roles << @role_non_member
336 340 end
337 341 else
338 342 @role_anonymous ||= Role.anonymous
339 343 roles << @role_anonymous
340 344 end
341 345 roles
342 346 end
343 347
344 348 # Return true if the user is a member of project
345 349 def member_of?(project)
346 350 !roles_for_project(project).detect {|role| role.member?}.nil?
347 351 end
348 352
349 353 # Return true if the user is allowed to do the specified action on a specific context
350 354 # Action can be:
351 355 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
352 356 # * a permission Symbol (eg. :edit_project)
353 357 # Context can be:
354 358 # * a project : returns true if user is allowed to do the specified action on this project
355 359 # * a group of projects : returns true if user is allowed on every project
356 360 # * nil with options[:global] set : check if user has at least one role allowed for this action,
357 361 # or falls back to Non Member / Anonymous permissions depending if the user is logged
358 362 def allowed_to?(action, context, options={})
359 363 if context && context.is_a?(Project)
360 364 # No action allowed on archived projects
361 365 return false unless context.active?
362 366 # No action allowed on disabled modules
363 367 return false unless context.allows_to?(action)
364 368 # Admin users are authorized for anything else
365 369 return true if admin?
366 370
367 371 roles = roles_for_project(context)
368 372 return false unless roles
369 373 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
370 374
371 375 elsif context && context.is_a?(Array)
372 376 # Authorize if user is authorized on every element of the array
373 377 context.map do |project|
374 378 allowed_to?(action,project,options)
375 379 end.inject do |memo,allowed|
376 380 memo && allowed
377 381 end
378 382 elsif options[:global]
379 383 # Admin users are always authorized
380 384 return true if admin?
381 385
382 386 # authorize if user has at least one role that has this permission
383 387 roles = memberships.collect {|m| m.roles}.flatten.uniq
384 388 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
385 389 else
386 390 false
387 391 end
388 392 end
389 393
390 394 # Is the user allowed to do the specified action on any project?
391 395 # See allowed_to? for the actions and valid options.
392 396 def allowed_to_globally?(action, options)
393 397 allowed_to?(action, nil, options.reverse_merge(:global => true))
394 398 end
395 399
396 400 safe_attributes 'login',
397 401 'firstname',
398 402 'lastname',
399 403 'mail',
400 404 'mail_notification',
401 405 'language',
402 406 'custom_field_values',
403 407 'custom_fields',
404 408 'identity_url'
405 409
406 410 safe_attributes 'status',
407 411 'auth_source_id',
408 412 :if => lambda {|user, current_user| current_user.admin?}
409 413
410 414 safe_attributes 'group_ids',
411 415 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
412 416
413 417 # Utility method to help check if a user should be notified about an
414 418 # event.
415 419 #
416 420 # TODO: only supports Issue events currently
417 421 def notify_about?(object)
418 422 case mail_notification
419 423 when 'all'
420 424 true
421 425 when 'selected'
422 426 # user receives notifications for created/assigned issues on unselected projects
423 427 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
424 428 true
425 429 else
426 430 false
427 431 end
428 432 when 'none'
429 433 false
430 434 when 'only_my_events'
431 435 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
432 436 true
433 437 else
434 438 false
435 439 end
436 440 when 'only_assigned'
437 441 if object.is_a?(Issue) && object.assigned_to == self
438 442 true
439 443 else
440 444 false
441 445 end
442 446 when 'only_owner'
443 447 if object.is_a?(Issue) && object.author == self
444 448 true
445 449 else
446 450 false
447 451 end
448 452 else
449 453 false
450 454 end
451 455 end
452 456
453 457 def self.current=(user)
454 458 @current_user = user
455 459 end
456 460
457 461 def self.current
458 462 @current_user ||= User.anonymous
459 463 end
460 464
461 465 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
462 466 # one anonymous user per database.
463 467 def self.anonymous
464 468 anonymous_user = AnonymousUser.find(:first)
465 469 if anonymous_user.nil?
466 470 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
467 471 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
468 472 end
469 473 anonymous_user
470 474 end
471 475
472 476 protected
473 477
474 478 def validate
475 479 # Password length validation based on setting
476 480 if !password.nil? && password.size < Setting.password_min_length.to_i
477 481 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
478 482 end
479 483 end
480 484
481 485 private
482 486
483 487 # Return password digest
484 488 def self.hash_password(clear_password)
485 489 Digest::SHA1.hexdigest(clear_password || "")
486 490 end
487 491 end
488 492
489 493 class AnonymousUser < User
490 494
491 495 def validate_on_create
492 496 # There should be only one AnonymousUser in the database
493 497 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
494 498 end
495 499
496 500 def available_custom_fields
497 501 []
498 502 end
499 503
500 504 # Overrides a few properties
501 505 def logged?; false end
502 506 def admin; false end
503 507 def name(*args); I18n.t(:label_user_anonymous) end
504 508 def mail; nil end
505 509 def time_zone; nil end
506 510 def rss_key; nil end
507 511 end
@@ -1,42 +1,42
1 1 <% if @deliveries %>
2 2 <% form_tag({:action => 'edit', :tab => 'notifications'}) do %>
3 3
4 4 <div class="box tabular settings">
5 5 <p><%= setting_text_field :mail_from, :size => 60 %></p>
6 6
7 7 <p><%= setting_check_box :bcc_recipients %></p>
8 8
9 9 <p><%= setting_check_box :plain_text_mail %></p>
10 10
11 <p><%= setting_select(:default_notification_option, User::MAIL_NOTIFICATION_OPTIONS.collect {|o| [l(o.last), o.first.to_s]}) %></p>
11 <p><%= setting_select(:default_notification_option, User.valid_notification_options.collect {|o| [l(o.last), o.first.to_s]}) %></p>
12 12
13 13 </div>
14 14
15 15 <fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend>
16 16 <%= hidden_field_tag 'settings[notified_events][]', '' %>
17 17 <% @notifiables.each do |notifiable| %>
18 18 <%= notification_field notifiable %>
19 19 <br />
20 20 <% end %>
21 21 <p><%= check_all_links('notified_events') %></p>
22 22 </fieldset>
23 23
24 24 <fieldset class="box"><legend><%= l(:setting_emails_header) %></legend>
25 25 <%= setting_text_area :emails_header, :label => false, :class => 'wiki-edit', :rows => 5 %>
26 26 </fieldset>
27 27
28 28 <fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend>
29 29 <%= setting_text_area :emails_footer, :label => false, :class => 'wiki-edit', :rows => 5 %>
30 30 </fieldset>
31 31
32 32 <div style="float:right;">
33 33 <%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %>
34 34 </div>
35 35
36 36 <%= submit_tag l(:button_save) %>
37 37 <% end %>
38 38 <% else %>
39 39 <div class="nodata">
40 40 <%= simple_format(l(:text_email_delivery_not_configured)) %>
41 41 </div>
42 42 <% end %>
@@ -1,549 +1,562
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
1 # Redmine - project management software
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
23 23 def setup
24 24 @admin = User.find(1)
25 25 @jsmith = User.find(2)
26 26 @dlopper = User.find(3)
27 27 end
28 28
29 29 test 'object_daddy creation' do
30 30 User.generate_with_protected!(:firstname => 'Testing connection')
31 31 User.generate_with_protected!(:firstname => 'Testing connection')
32 32 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
33 33 end
34 34
35 35 def test_truth
36 36 assert_kind_of User, @jsmith
37 37 end
38 38
39 39 def test_mail_should_be_stripped
40 40 u = User.new
41 41 u.mail = " foo@bar.com "
42 42 assert_equal "foo@bar.com", u.mail
43 43 end
44 44
45 45 def test_create
46 46 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
47 47
48 48 user.login = "jsmith"
49 49 user.password, user.password_confirmation = "password", "password"
50 50 # login uniqueness
51 51 assert !user.save
52 52 assert_equal 1, user.errors.count
53 53
54 54 user.login = "newuser"
55 55 user.password, user.password_confirmation = "passwd", "password"
56 56 # password confirmation
57 57 assert !user.save
58 58 assert_equal 1, user.errors.count
59 59
60 60 user.password, user.password_confirmation = "password", "password"
61 61 assert user.save
62 62 end
63 63
64 64 context "User#before_create" do
65 65 should "set the mail_notification to the default Setting" do
66 66 @user1 = User.generate_with_protected!
67 67 assert_equal 'only_my_events', @user1.mail_notification
68 68
69 69 with_settings :default_notification_option => 'all' do
70 70 @user2 = User.generate_with_protected!
71 71 assert_equal 'all', @user2.mail_notification
72 72 end
73 73 end
74 74 end
75 75
76 76 context "User.login" do
77 77 should "be case-insensitive." do
78 78 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
79 79 u.login = 'newuser'
80 80 u.password, u.password_confirmation = "password", "password"
81 81 assert u.save
82 82
83 83 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
84 84 u.login = 'NewUser'
85 85 u.password, u.password_confirmation = "password", "password"
86 86 assert !u.save
87 87 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:login)
88 88 end
89 89 end
90 90
91 91 def test_mail_uniqueness_should_not_be_case_sensitive
92 92 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
93 93 u.login = 'newuser1'
94 94 u.password, u.password_confirmation = "password", "password"
95 95 assert u.save
96 96
97 97 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
98 98 u.login = 'newuser2'
99 99 u.password, u.password_confirmation = "password", "password"
100 100 assert !u.save
101 101 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
102 102 end
103 103
104 104 def test_update
105 105 assert_equal "admin", @admin.login
106 106 @admin.login = "john"
107 107 assert @admin.save, @admin.errors.full_messages.join("; ")
108 108 @admin.reload
109 109 assert_equal "john", @admin.login
110 110 end
111 111
112 112 def test_destroy
113 113 User.find(2).destroy
114 114 assert_nil User.find_by_id(2)
115 115 assert Member.find_all_by_user_id(2).empty?
116 116 end
117 117
118 118 def test_validate_login_presence
119 119 @admin.login = ""
120 120 assert !@admin.save
121 121 assert_equal 1, @admin.errors.count
122 122 end
123 123
124 124 def test_validate_mail_notification_inclusion
125 125 u = User.new
126 126 u.mail_notification = 'foo'
127 127 u.save
128 128 assert_not_nil u.errors.on(:mail_notification)
129 129 end
130 130
131 131 context "User#try_to_login" do
132 132 should "fall-back to case-insensitive if user login is not found as-typed." do
133 133 user = User.try_to_login("AdMin", "admin")
134 134 assert_kind_of User, user
135 135 assert_equal "admin", user.login
136 136 end
137 137
138 138 should "select the exact matching user first" do
139 139 case_sensitive_user = User.generate_with_protected!(:login => 'changed', :password => 'admin', :password_confirmation => 'admin')
140 140 # bypass validations to make it appear like existing data
141 141 case_sensitive_user.update_attribute(:login, 'ADMIN')
142 142
143 143 user = User.try_to_login("ADMIN", "admin")
144 144 assert_kind_of User, user
145 145 assert_equal "ADMIN", user.login
146 146
147 147 end
148 148 end
149 149
150 150 def test_password
151 151 user = User.try_to_login("admin", "admin")
152 152 assert_kind_of User, user
153 153 assert_equal "admin", user.login
154 154 user.password = "hello"
155 155 assert user.save
156 156
157 157 user = User.try_to_login("admin", "hello")
158 158 assert_kind_of User, user
159 159 assert_equal "admin", user.login
160 160 assert_equal User.hash_password("hello"), user.hashed_password
161 161 end
162 162
163 163 def test_name_format
164 164 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
165 165 Setting.user_format = :firstname_lastname
166 166 assert_equal 'John Smith', @jsmith.reload.name
167 167 Setting.user_format = :username
168 168 assert_equal 'jsmith', @jsmith.reload.name
169 169 end
170 170
171 171 def test_lock
172 172 user = User.try_to_login("jsmith", "jsmith")
173 173 assert_equal @jsmith, user
174 174
175 175 @jsmith.status = User::STATUS_LOCKED
176 176 assert @jsmith.save
177 177
178 178 user = User.try_to_login("jsmith", "jsmith")
179 179 assert_equal nil, user
180 180 end
181 181
182 182 if ldap_configured?
183 183 context "#try_to_login using LDAP" do
184 184 context "with failed connection to the LDAP server" do
185 185 should "return nil" do
186 186 @auth_source = AuthSourceLdap.find(1)
187 187 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
188 188
189 189 assert_equal nil, User.try_to_login('edavis', 'wrong')
190 190 end
191 191 end
192 192
193 193 context "with an unsuccessful authentication" do
194 194 should "return nil" do
195 195 assert_equal nil, User.try_to_login('edavis', 'wrong')
196 196 end
197 197 end
198 198
199 199 context "on the fly registration" do
200 200 setup do
201 201 @auth_source = AuthSourceLdap.find(1)
202 202 end
203 203
204 204 context "with a successful authentication" do
205 205 should "create a new user account if it doesn't exist" do
206 206 assert_difference('User.count') do
207 207 user = User.try_to_login('edavis', '123456')
208 208 assert !user.admin?
209 209 end
210 210 end
211 211
212 212 should "retrieve existing user" do
213 213 user = User.try_to_login('edavis', '123456')
214 214 user.admin = true
215 215 user.save!
216 216
217 217 assert_no_difference('User.count') do
218 218 user = User.try_to_login('edavis', '123456')
219 219 assert user.admin?
220 220 end
221 221 end
222 222 end
223 223 end
224 224 end
225 225
226 226 else
227 227 puts "Skipping LDAP tests."
228 228 end
229 229
230 230 def test_create_anonymous
231 231 AnonymousUser.delete_all
232 232 anon = User.anonymous
233 233 assert !anon.new_record?
234 234 assert_kind_of AnonymousUser, anon
235 235 end
236 236
237 237 should_have_one :rss_token
238 238
239 239 def test_rss_key
240 240 assert_nil @jsmith.rss_token
241 241 key = @jsmith.rss_key
242 242 assert_equal 40, key.length
243 243
244 244 @jsmith.reload
245 245 assert_equal key, @jsmith.rss_key
246 246 end
247 247
248 248
249 249 should_have_one :api_token
250 250
251 251 context "User#api_key" do
252 252 should "generate a new one if the user doesn't have one" do
253 253 user = User.generate_with_protected!(:api_token => nil)
254 254 assert_nil user.api_token
255 255
256 256 key = user.api_key
257 257 assert_equal 40, key.length
258 258 user.reload
259 259 assert_equal key, user.api_key
260 260 end
261 261
262 262 should "return the existing api token value" do
263 263 user = User.generate_with_protected!
264 264 token = Token.generate!(:action => 'api')
265 265 user.api_token = token
266 266 assert user.save
267 267
268 268 assert_equal token.value, user.api_key
269 269 end
270 270 end
271 271
272 272 context "User#find_by_api_key" do
273 273 should "return nil if no matching key is found" do
274 274 assert_nil User.find_by_api_key('zzzzzzzzz')
275 275 end
276 276
277 277 should "return nil if the key is found for an inactive user" do
278 278 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
279 279 token = Token.generate!(:action => 'api')
280 280 user.api_token = token
281 281 user.save
282 282
283 283 assert_nil User.find_by_api_key(token.value)
284 284 end
285 285
286 286 should "return the user if the key is found for an active user" do
287 287 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
288 288 token = Token.generate!(:action => 'api')
289 289 user.api_token = token
290 290 user.save
291 291
292 292 assert_equal user, User.find_by_api_key(token.value)
293 293 end
294 294 end
295 295
296 296 def test_roles_for_project
297 297 # user with a role
298 298 roles = @jsmith.roles_for_project(Project.find(1))
299 299 assert_kind_of Role, roles.first
300 300 assert_equal "Manager", roles.first.name
301 301
302 302 # user with no role
303 303 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
304 304 end
305 305
306 def test_valid_notification_options
307 # without memberships
308 assert_equal 5, User.find(7).valid_notification_options.size
309 # with memberships
310 assert_equal 6, User.find(2).valid_notification_options.size
311 end
312
313 def test_valid_notification_options_class_method
314 assert_equal 5, User.valid_notification_options.size
315 assert_equal 5, User.valid_notification_options(User.find(7)).size
316 assert_equal 6, User.valid_notification_options(User.find(2)).size
317 end
318
306 319 def test_mail_notification_all
307 320 @jsmith.mail_notification = 'all'
308 321 @jsmith.notified_project_ids = []
309 322 @jsmith.save
310 323 @jsmith.reload
311 324 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
312 325 end
313 326
314 327 def test_mail_notification_selected
315 328 @jsmith.mail_notification = 'selected'
316 329 @jsmith.notified_project_ids = [1]
317 330 @jsmith.save
318 331 @jsmith.reload
319 332 assert Project.find(1).recipients.include?(@jsmith.mail)
320 333 end
321 334
322 335 def test_mail_notification_only_my_events
323 336 @jsmith.mail_notification = 'only_my_events'
324 337 @jsmith.notified_project_ids = []
325 338 @jsmith.save
326 339 @jsmith.reload
327 340 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
328 341 end
329 342
330 343 def test_comments_sorting_preference
331 344 assert !@jsmith.wants_comments_in_reverse_order?
332 345 @jsmith.pref.comments_sorting = 'asc'
333 346 assert !@jsmith.wants_comments_in_reverse_order?
334 347 @jsmith.pref.comments_sorting = 'desc'
335 348 assert @jsmith.wants_comments_in_reverse_order?
336 349 end
337 350
338 351 def test_find_by_mail_should_be_case_insensitive
339 352 u = User.find_by_mail('JSmith@somenet.foo')
340 353 assert_not_nil u
341 354 assert_equal 'jsmith@somenet.foo', u.mail
342 355 end
343 356
344 357 def test_random_password
345 358 u = User.new
346 359 u.random_password
347 360 assert !u.password.blank?
348 361 assert !u.password_confirmation.blank?
349 362 end
350 363
351 364 context "#change_password_allowed?" do
352 365 should "be allowed if no auth source is set" do
353 366 user = User.generate_with_protected!
354 367 assert user.change_password_allowed?
355 368 end
356 369
357 370 should "delegate to the auth source" do
358 371 user = User.generate_with_protected!
359 372
360 373 allowed_auth_source = AuthSource.generate!
361 374 def allowed_auth_source.allow_password_changes?; true; end
362 375
363 376 denied_auth_source = AuthSource.generate!
364 377 def denied_auth_source.allow_password_changes?; false; end
365 378
366 379 assert user.change_password_allowed?
367 380
368 381 user.auth_source = allowed_auth_source
369 382 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
370 383
371 384 user.auth_source = denied_auth_source
372 385 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
373 386 end
374 387
375 388 end
376 389
377 390 context "#allowed_to?" do
378 391 context "with a unique project" do
379 392 should "return false if project is archived" do
380 393 project = Project.find(1)
381 394 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
382 395 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
383 396 end
384 397
385 398 should "return false if related module is disabled" do
386 399 project = Project.find(1)
387 400 project.enabled_module_names = ["issue_tracking"]
388 401 assert @admin.allowed_to?(:add_issues, project)
389 402 assert ! @admin.allowed_to?(:view_wiki_pages, project)
390 403 end
391 404
392 405 should "authorize nearly everything for admin users" do
393 406 project = Project.find(1)
394 407 assert ! @admin.member_of?(project)
395 408 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
396 409 assert @admin.allowed_to?(p.to_sym, project)
397 410 end
398 411 end
399 412
400 413 should "authorize normal users depending on their roles" do
401 414 project = Project.find(1)
402 415 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
403 416 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
404 417 end
405 418 end
406 419
407 420 context "with multiple projects" do
408 421 should "return false if array is empty" do
409 422 assert ! @admin.allowed_to?(:view_project, [])
410 423 end
411 424
412 425 should "return true only if user has permission on all these projects" do
413 426 assert @admin.allowed_to?(:view_project, Project.all)
414 427 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
415 428 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
416 429 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
417 430 end
418 431
419 432 should "behave correctly with arrays of 1 project" do
420 433 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
421 434 end
422 435 end
423 436
424 437 context "with options[:global]" do
425 438 should "authorize if user has at least one role that has this permission" do
426 439 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
427 440 @anonymous = User.find(6)
428 441 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
429 442 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
430 443 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
431 444 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
432 445 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
433 446 end
434 447 end
435 448 end
436 449
437 450 context "User#notify_about?" do
438 451 context "Issues" do
439 452 setup do
440 453 @project = Project.find(1)
441 454 @author = User.generate_with_protected!
442 455 @assignee = User.generate_with_protected!
443 456 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
444 457 end
445 458
446 459 should "be true for a user with :all" do
447 460 @author.update_attribute(:mail_notification, 'all')
448 461 assert @author.notify_about?(@issue)
449 462 end
450 463
451 464 should "be false for a user with :none" do
452 465 @author.update_attribute(:mail_notification, 'none')
453 466 assert ! @author.notify_about?(@issue)
454 467 end
455 468
456 469 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
457 470 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
458 471 Member.create!(:user => @user, :project => @project, :role_ids => [1])
459 472 assert ! @user.notify_about?(@issue)
460 473 end
461 474
462 475 should "be true for a user with :only_my_events and is the author" do
463 476 @author.update_attribute(:mail_notification, 'only_my_events')
464 477 assert @author.notify_about?(@issue)
465 478 end
466 479
467 480 should "be true for a user with :only_my_events and is the assignee" do
468 481 @assignee.update_attribute(:mail_notification, 'only_my_events')
469 482 assert @assignee.notify_about?(@issue)
470 483 end
471 484
472 485 should "be true for a user with :only_assigned and is the assignee" do
473 486 @assignee.update_attribute(:mail_notification, 'only_assigned')
474 487 assert @assignee.notify_about?(@issue)
475 488 end
476 489
477 490 should "be false for a user with :only_assigned and is not the assignee" do
478 491 @author.update_attribute(:mail_notification, 'only_assigned')
479 492 assert ! @author.notify_about?(@issue)
480 493 end
481 494
482 495 should "be true for a user with :only_owner and is the author" do
483 496 @author.update_attribute(:mail_notification, 'only_owner')
484 497 assert @author.notify_about?(@issue)
485 498 end
486 499
487 500 should "be false for a user with :only_owner and is not the author" do
488 501 @assignee.update_attribute(:mail_notification, 'only_owner')
489 502 assert ! @assignee.notify_about?(@issue)
490 503 end
491 504
492 505 should "be true for a user with :selected and is the author" do
493 506 @author.update_attribute(:mail_notification, 'selected')
494 507 assert @author.notify_about?(@issue)
495 508 end
496 509
497 510 should "be true for a user with :selected and is the assignee" do
498 511 @assignee.update_attribute(:mail_notification, 'selected')
499 512 assert @assignee.notify_about?(@issue)
500 513 end
501 514
502 515 should "be false for a user with :selected and is not the author or assignee" do
503 516 @user = User.generate_with_protected!(:mail_notification => 'selected')
504 517 Member.create!(:user => @user, :project => @project, :role_ids => [1])
505 518 assert ! @user.notify_about?(@issue)
506 519 end
507 520 end
508 521
509 522 context "other events" do
510 523 should 'be added and tested'
511 524 end
512 525 end
513 526
514 527 if Object.const_defined?(:OpenID)
515 528
516 529 def test_setting_identity_url
517 530 normalized_open_id_url = 'http://example.com/'
518 531 u = User.new( :identity_url => 'http://example.com/' )
519 532 assert_equal normalized_open_id_url, u.identity_url
520 533 end
521 534
522 535 def test_setting_identity_url_without_trailing_slash
523 536 normalized_open_id_url = 'http://example.com/'
524 537 u = User.new( :identity_url => 'http://example.com' )
525 538 assert_equal normalized_open_id_url, u.identity_url
526 539 end
527 540
528 541 def test_setting_identity_url_without_protocol
529 542 normalized_open_id_url = 'http://example.com/'
530 543 u = User.new( :identity_url => 'example.com' )
531 544 assert_equal normalized_open_id_url, u.identity_url
532 545 end
533 546
534 547 def test_setting_blank_identity_url
535 548 u = User.new( :identity_url => 'example.com' )
536 549 u.identity_url = ''
537 550 assert u.identity_url.blank?
538 551 end
539 552
540 553 def test_setting_invalid_identity_url
541 554 u = User.new( :identity_url => 'this is not an openid url' )
542 555 assert u.identity_url.blank?
543 556 end
544 557
545 558 else
546 559 puts "Skipping openid tests."
547 560 end
548 561
549 562 end
General Comments 0
You need to be logged in to leave comments. Login now