##// END OF EJS Templates
Fix LDAP on the fly creation. The User object doesn't have a :dn attribute....
Eric Davis -
r3371:19d4ddf2f215
parent child
Show More
@@ -1,360 +1,361
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
22 22 # Account statuses
23 23 STATUS_ANONYMOUS = 0
24 24 STATUS_ACTIVE = 1
25 25 STATUS_REGISTERED = 2
26 26 STATUS_LOCKED = 3
27 27
28 28 USER_FORMATS = {
29 29 :firstname_lastname => '#{firstname} #{lastname}',
30 30 :firstname => '#{firstname}',
31 31 :lastname_firstname => '#{lastname} #{firstname}',
32 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 33 :username => '#{login}'
34 34 }
35 35
36 36 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
37 37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
38 38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
39 39 has_many :changesets, :dependent => :nullify
40 40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 42 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
43 43 belongs_to :auth_source
44 44
45 45 # Active non-anonymous users scope
46 46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47 47
48 48 acts_as_customizable
49 49
50 50 attr_accessor :password, :password_confirmation
51 51 attr_accessor :last_before_login_on
52 52 # Prevents unauthorized assignments
53 53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
54 54
55 55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
57 57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
58 58 # Login must contain lettres, numbers, underscores only
59 59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 60 validates_length_of :login, :maximum => 30
61 61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 62 validates_length_of :firstname, :lastname, :maximum => 30
63 63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 64 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 65 validates_confirmation_of :password, :allow_nil => true
66 66
67 67 def before_create
68 68 self.mail_notification = false
69 69 true
70 70 end
71 71
72 72 def before_save
73 73 # update hashed_password if password was set
74 74 self.hashed_password = User.hash_password(self.password) if self.password
75 75 end
76 76
77 77 def reload(*args)
78 78 @name = nil
79 79 super
80 80 end
81 81
82 82 def identity_url=(url)
83 83 if url.blank?
84 84 write_attribute(:identity_url, '')
85 85 else
86 86 begin
87 87 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
88 88 rescue OpenIdAuthentication::InvalidOpenId
89 89 # Invlaid url, don't save
90 90 end
91 91 end
92 92 self.read_attribute(:identity_url)
93 93 end
94 94
95 95 # Returns the user that matches provided login and password, or nil
96 96 def self.try_to_login(login, password)
97 97 # Make sure no one can sign in with an empty password
98 98 return nil if password.to_s.empty?
99 99 user = find(:first, :conditions => ["login=?", login])
100 100 if user
101 101 # user is already in local database
102 102 return nil if !user.active?
103 103 if user.auth_source
104 104 # user has an external authentication method
105 105 return nil unless user.auth_source.authenticate(login, password)
106 106 else
107 107 # authentication with local password
108 108 return nil unless User.hash_password(password) == user.hashed_password
109 109 end
110 110 else
111 111 # user is not yet registered, try to authenticate with available sources
112 112 attrs = AuthSource.authenticate(login, password)
113 113 if attrs
114 user = new(*attrs)
114 attributes = *attrs
115 user = new(attributes.symbolize_keys.except(:dn))
115 116 user.login = login
116 117 user.language = Setting.default_language
117 118 if user.save
118 119 user.reload
119 120 logger.info("User '#{user.login}' created from the LDAP") if logger
120 121 end
121 122 end
122 123 end
123 124 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
124 125 user
125 126 rescue => text
126 127 raise text
127 128 end
128 129
129 130 # Returns the user who matches the given autologin +key+ or nil
130 131 def self.try_to_autologin(key)
131 132 tokens = Token.find_all_by_action_and_value('autologin', key)
132 133 # Make sure there's only 1 token that matches the key
133 134 if tokens.size == 1
134 135 token = tokens.first
135 136 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
136 137 token.user.update_attribute(:last_login_on, Time.now)
137 138 token.user
138 139 end
139 140 end
140 141 end
141 142
142 143 # Return user's full name for display
143 144 def name(formatter = nil)
144 145 if formatter
145 146 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
146 147 else
147 148 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
148 149 end
149 150 end
150 151
151 152 def active?
152 153 self.status == STATUS_ACTIVE
153 154 end
154 155
155 156 def registered?
156 157 self.status == STATUS_REGISTERED
157 158 end
158 159
159 160 def locked?
160 161 self.status == STATUS_LOCKED
161 162 end
162 163
163 164 def check_password?(clear_password)
164 165 User.hash_password(clear_password) == self.hashed_password
165 166 end
166 167
167 168 # Generate and set a random password. Useful for automated user creation
168 169 # Based on Token#generate_token_value
169 170 #
170 171 def random_password
171 172 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
172 173 password = ''
173 174 40.times { |i| password << chars[rand(chars.size-1)] }
174 175 self.password = password
175 176 self.password_confirmation = password
176 177 self
177 178 end
178 179
179 180 def pref
180 181 self.preference ||= UserPreference.new(:user => self)
181 182 end
182 183
183 184 def time_zone
184 185 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
185 186 end
186 187
187 188 def wants_comments_in_reverse_order?
188 189 self.pref[:comments_sorting] == 'desc'
189 190 end
190 191
191 192 # Return user's RSS key (a 40 chars long string), used to access feeds
192 193 def rss_key
193 194 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
194 195 token.value
195 196 end
196 197
197 198 # Return user's API key (a 40 chars long string), used to access the API
198 199 def api_key
199 200 token = self.api_token || self.create_api_token(:action => 'api')
200 201 token.value
201 202 end
202 203
203 204 # Return an array of project ids for which the user has explicitly turned mail notifications on
204 205 def notified_projects_ids
205 206 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
206 207 end
207 208
208 209 def notified_project_ids=(ids)
209 210 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
210 211 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
211 212 @notified_projects_ids = nil
212 213 notified_projects_ids
213 214 end
214 215
215 216 def self.find_by_rss_key(key)
216 217 token = Token.find_by_value(key)
217 218 token && token.user.active? ? token.user : nil
218 219 end
219 220
220 221 def self.find_by_api_key(key)
221 222 token = Token.find_by_action_and_value('api', key)
222 223 token && token.user.active? ? token.user : nil
223 224 end
224 225
225 226 # Makes find_by_mail case-insensitive
226 227 def self.find_by_mail(mail)
227 228 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
228 229 end
229 230
230 231 def to_s
231 232 name
232 233 end
233 234
234 235 # Returns the current day according to user's time zone
235 236 def today
236 237 if time_zone.nil?
237 238 Date.today
238 239 else
239 240 Time.now.in_time_zone(time_zone).to_date
240 241 end
241 242 end
242 243
243 244 def logged?
244 245 true
245 246 end
246 247
247 248 def anonymous?
248 249 !logged?
249 250 end
250 251
251 252 # Return user's roles for project
252 253 def roles_for_project(project)
253 254 roles = []
254 255 # No role on archived projects
255 256 return roles unless project && project.active?
256 257 if logged?
257 258 # Find project membership
258 259 membership = memberships.detect {|m| m.project_id == project.id}
259 260 if membership
260 261 roles = membership.roles
261 262 else
262 263 @role_non_member ||= Role.non_member
263 264 roles << @role_non_member
264 265 end
265 266 else
266 267 @role_anonymous ||= Role.anonymous
267 268 roles << @role_anonymous
268 269 end
269 270 roles
270 271 end
271 272
272 273 # Return true if the user is a member of project
273 274 def member_of?(project)
274 275 !roles_for_project(project).detect {|role| role.member?}.nil?
275 276 end
276 277
277 278 # Return true if the user is allowed to do the specified action on project
278 279 # action can be:
279 280 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
280 281 # * a permission Symbol (eg. :edit_project)
281 282 def allowed_to?(action, project, options={})
282 283 if project
283 284 # No action allowed on archived projects
284 285 return false unless project.active?
285 286 # No action allowed on disabled modules
286 287 return false unless project.allows_to?(action)
287 288 # Admin users are authorized for anything else
288 289 return true if admin?
289 290
290 291 roles = roles_for_project(project)
291 292 return false unless roles
292 293 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
293 294
294 295 elsif options[:global]
295 296 # Admin users are always authorized
296 297 return true if admin?
297 298
298 299 # authorize if user has at least one role that has this permission
299 300 roles = memberships.collect {|m| m.roles}.flatten.uniq
300 301 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
301 302 else
302 303 false
303 304 end
304 305 end
305 306
306 307 def self.current=(user)
307 308 @current_user = user
308 309 end
309 310
310 311 def self.current
311 312 @current_user ||= User.anonymous
312 313 end
313 314
314 315 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
315 316 # one anonymous user per database.
316 317 def self.anonymous
317 318 anonymous_user = AnonymousUser.find(:first)
318 319 if anonymous_user.nil?
319 320 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
320 321 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
321 322 end
322 323 anonymous_user
323 324 end
324 325
325 326 protected
326 327
327 328 def validate
328 329 # Password length validation based on setting
329 330 if !password.nil? && password.size < Setting.password_min_length.to_i
330 331 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
331 332 end
332 333 end
333 334
334 335 private
335 336
336 337 # Return password digest
337 338 def self.hash_password(clear_password)
338 339 Digest::SHA1.hexdigest(clear_password || "")
339 340 end
340 341 end
341 342
342 343 class AnonymousUser < User
343 344
344 345 def validate_on_create
345 346 # There should be only one AnonymousUser in the database
346 347 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
347 348 end
348 349
349 350 def available_custom_fields
350 351 []
351 352 end
352 353
353 354 # Overrides a few properties
354 355 def logged?; false end
355 356 def admin; false end
356 357 def name(*args); I18n.t(:label_user_anonymous) end
357 358 def mail; nil end
358 359 def time_zone; nil end
359 360 def rss_key; nil end
360 361 end
@@ -1,279 +1,309
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class UserTest < ActiveSupport::TestCase
21 21 fixtures :users, :members, :projects, :roles, :member_roles
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_create
40 40 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
41 41
42 42 user.login = "jsmith"
43 43 user.password, user.password_confirmation = "password", "password"
44 44 # login uniqueness
45 45 assert !user.save
46 46 assert_equal 1, user.errors.count
47 47
48 48 user.login = "newuser"
49 49 user.password, user.password_confirmation = "passwd", "password"
50 50 # password confirmation
51 51 assert !user.save
52 52 assert_equal 1, user.errors.count
53 53
54 54 user.password, user.password_confirmation = "password", "password"
55 55 assert user.save
56 56 end
57 57
58 58 def test_mail_uniqueness_should_not_be_case_sensitive
59 59 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
60 60 u.login = 'newuser1'
61 61 u.password, u.password_confirmation = "password", "password"
62 62 assert u.save
63 63
64 64 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
65 65 u.login = 'newuser2'
66 66 u.password, u.password_confirmation = "password", "password"
67 67 assert !u.save
68 68 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
69 69 end
70 70
71 71 def test_update
72 72 assert_equal "admin", @admin.login
73 73 @admin.login = "john"
74 74 assert @admin.save, @admin.errors.full_messages.join("; ")
75 75 @admin.reload
76 76 assert_equal "john", @admin.login
77 77 end
78 78
79 79 def test_destroy
80 80 User.find(2).destroy
81 81 assert_nil User.find_by_id(2)
82 82 assert Member.find_all_by_user_id(2).empty?
83 83 end
84 84
85 85 def test_validate
86 86 @admin.login = ""
87 87 assert !@admin.save
88 88 assert_equal 1, @admin.errors.count
89 89 end
90 90
91 91 def test_password
92 92 user = User.try_to_login("admin", "admin")
93 93 assert_kind_of User, user
94 94 assert_equal "admin", user.login
95 95 user.password = "hello"
96 96 assert user.save
97 97
98 98 user = User.try_to_login("admin", "hello")
99 99 assert_kind_of User, user
100 100 assert_equal "admin", user.login
101 101 assert_equal User.hash_password("hello"), user.hashed_password
102 102 end
103 103
104 104 def test_name_format
105 105 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
106 106 Setting.user_format = :firstname_lastname
107 107 assert_equal 'John Smith', @jsmith.reload.name
108 108 Setting.user_format = :username
109 109 assert_equal 'jsmith', @jsmith.reload.name
110 110 end
111 111
112 112 def test_lock
113 113 user = User.try_to_login("jsmith", "jsmith")
114 114 assert_equal @jsmith, user
115 115
116 116 @jsmith.status = User::STATUS_LOCKED
117 117 assert @jsmith.save
118 118
119 119 user = User.try_to_login("jsmith", "jsmith")
120 120 assert_equal nil, user
121 121 end
122 122
123 if ldap_configured?
124 context "#try_to_login using LDAP" do
125 context "on the fly registration" do
126 setup do
127 @auth_source = AuthSourceLdap.generate!(:name => 'localhost',
128 :host => '127.0.0.1',
129 :port => 389,
130 :base_dn => 'OU=Person,DC=redmine,DC=org',
131 :attr_login => 'uid',
132 :attr_firstname => 'givenName',
133 :attr_lastname => 'sn',
134 :attr_mail => 'mail',
135 :onthefly_register => true)
136
137 end
138
139 context "with a successful authentication" do
140 should "create a new user account" do
141 assert_difference('User.count') do
142 User.try_to_login('edavis', '123456')
143 end
144 end
145 end
146 end
147 end
148
149 else
150 puts "Skipping LDAP tests."
151 end
152
123 153 def test_create_anonymous
124 154 AnonymousUser.delete_all
125 155 anon = User.anonymous
126 156 assert !anon.new_record?
127 157 assert_kind_of AnonymousUser, anon
128 158 end
129 159
130 160 should_have_one :rss_token
131 161
132 162 def test_rss_key
133 163 assert_nil @jsmith.rss_token
134 164 key = @jsmith.rss_key
135 165 assert_equal 40, key.length
136 166
137 167 @jsmith.reload
138 168 assert_equal key, @jsmith.rss_key
139 169 end
140 170
141 171
142 172 should_have_one :api_token
143 173
144 174 context "User#api_key" do
145 175 should "generate a new one if the user doesn't have one" do
146 176 user = User.generate_with_protected!(:api_token => nil)
147 177 assert_nil user.api_token
148 178
149 179 key = user.api_key
150 180 assert_equal 40, key.length
151 181 user.reload
152 182 assert_equal key, user.api_key
153 183 end
154 184
155 185 should "return the existing api token value" do
156 186 user = User.generate_with_protected!
157 187 token = Token.generate!(:action => 'api')
158 188 user.api_token = token
159 189 assert user.save
160 190
161 191 assert_equal token.value, user.api_key
162 192 end
163 193 end
164 194
165 195 context "User#find_by_api_key" do
166 196 should "return nil if no matching key is found" do
167 197 assert_nil User.find_by_api_key('zzzzzzzzz')
168 198 end
169 199
170 200 should "return nil if the key is found for an inactive user" do
171 201 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
172 202 token = Token.generate!(:action => 'api')
173 203 user.api_token = token
174 204 user.save
175 205
176 206 assert_nil User.find_by_api_key(token.value)
177 207 end
178 208
179 209 should "return the user if the key is found for an active user" do
180 210 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
181 211 token = Token.generate!(:action => 'api')
182 212 user.api_token = token
183 213 user.save
184 214
185 215 assert_equal user, User.find_by_api_key(token.value)
186 216 end
187 217 end
188 218
189 219 def test_roles_for_project
190 220 # user with a role
191 221 roles = @jsmith.roles_for_project(Project.find(1))
192 222 assert_kind_of Role, roles.first
193 223 assert_equal "Manager", roles.first.name
194 224
195 225 # user with no role
196 226 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
197 227 end
198 228
199 229 def test_mail_notification_all
200 230 @jsmith.mail_notification = true
201 231 @jsmith.notified_project_ids = []
202 232 @jsmith.save
203 233 @jsmith.reload
204 234 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
205 235 end
206 236
207 237 def test_mail_notification_selected
208 238 @jsmith.mail_notification = false
209 239 @jsmith.notified_project_ids = [1]
210 240 @jsmith.save
211 241 @jsmith.reload
212 242 assert Project.find(1).recipients.include?(@jsmith.mail)
213 243 end
214 244
215 245 def test_mail_notification_none
216 246 @jsmith.mail_notification = false
217 247 @jsmith.notified_project_ids = []
218 248 @jsmith.save
219 249 @jsmith.reload
220 250 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
221 251 end
222 252
223 253 def test_comments_sorting_preference
224 254 assert !@jsmith.wants_comments_in_reverse_order?
225 255 @jsmith.pref.comments_sorting = 'asc'
226 256 assert !@jsmith.wants_comments_in_reverse_order?
227 257 @jsmith.pref.comments_sorting = 'desc'
228 258 assert @jsmith.wants_comments_in_reverse_order?
229 259 end
230 260
231 261 def test_find_by_mail_should_be_case_insensitive
232 262 u = User.find_by_mail('JSmith@somenet.foo')
233 263 assert_not_nil u
234 264 assert_equal 'jsmith@somenet.foo', u.mail
235 265 end
236 266
237 267 def test_random_password
238 268 u = User.new
239 269 u.random_password
240 270 assert !u.password.blank?
241 271 assert !u.password_confirmation.blank?
242 272 end
243 273
244 274 if Object.const_defined?(:OpenID)
245 275
246 276 def test_setting_identity_url
247 277 normalized_open_id_url = 'http://example.com/'
248 278 u = User.new( :identity_url => 'http://example.com/' )
249 279 assert_equal normalized_open_id_url, u.identity_url
250 280 end
251 281
252 282 def test_setting_identity_url_without_trailing_slash
253 283 normalized_open_id_url = 'http://example.com/'
254 284 u = User.new( :identity_url => 'http://example.com' )
255 285 assert_equal normalized_open_id_url, u.identity_url
256 286 end
257 287
258 288 def test_setting_identity_url_without_protocol
259 289 normalized_open_id_url = 'http://example.com/'
260 290 u = User.new( :identity_url => 'example.com' )
261 291 assert_equal normalized_open_id_url, u.identity_url
262 292 end
263 293
264 294 def test_setting_blank_identity_url
265 295 u = User.new( :identity_url => 'example.com' )
266 296 u.identity_url = ''
267 297 assert u.identity_url.blank?
268 298 end
269 299
270 300 def test_setting_invalid_identity_url
271 301 u = User.new( :identity_url => 'this is not an openid url' )
272 302 assert u.identity_url.blank?
273 303 end
274 304
275 305 else
276 306 puts "Skipping openid tests."
277 307 end
278 308
279 309 end
General Comments 0
You need to be logged in to leave comments. Login now