login_sugar_generator 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- data/USAGE +32 -0
- data/login_sugar_generator.rb +79 -0
- data/templates/README +194 -0
- data/templates/_view_edit.rhtml +30 -0
- data/templates/_view_password.rhtml +21 -0
- data/templates/clock.rb +14 -0
- data/templates/controller.rb +179 -0
- data/templates/controller_test.rb +254 -0
- data/templates/create_db +7 -0
- data/templates/default_setup.zip +0 -0
- data/templates/helper.rb +41 -0
- data/templates/integration_test.rb +95 -0
- data/templates/layout.rhtml +13 -0
- data/templates/login_environment.rb +21 -0
- data/templates/login_system.rb +54 -0
- data/templates/migration_login_sugar.rb +21 -0
- data/templates/mock_clock.rb +14 -0
- data/templates/mock_notify.rb +16 -0
- data/templates/notify.rb +50 -0
- data/templates/notify_change_password.rhtml +10 -0
- data/templates/notify_delete.rhtml +5 -0
- data/templates/notify_forgot_password.rhtml +11 -0
- data/templates/notify_pending_delete.rhtml +9 -0
- data/templates/notify_signup.rhtml +12 -0
- data/templates/stylesheet.css +74 -0
- data/templates/user.rb +101 -0
- data/templates/user_model.erbsql +18 -0
- data/templates/user_test.rb +133 -0
- data/templates/users.yml +48 -0
- data/templates/view_change_password.rhtml +15 -0
- data/templates/view_edit.rhtml +19 -0
- data/templates/view_forgot_password.rhtml +19 -0
- data/templates/view_login.rhtml +24 -0
- data/templates/view_logout.rhtml +8 -0
- data/templates/view_signup.rhtml +14 -0
- data/templates/view_welcome.rhtml +10 -0
- metadata +80 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
class LoginSugar < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :<%= plural_name %>, :force => true do |t|
|
4
|
+
t.column :login, :string, :limit => 80, :null => false
|
5
|
+
t.column :salted_password, :string, :limit => 40, :null => false
|
6
|
+
t.column :email, :string, :limit => 60, :null => false
|
7
|
+
t.column :first_name, :string, :limit => 40
|
8
|
+
t.column :last_name, :string, :limit => 40
|
9
|
+
t.column :salt, 'CHAR(40)', :null => false
|
10
|
+
t.column :verified, :boolean, :default => false
|
11
|
+
t.column :role, :string, :limit => 40, :default => nil
|
12
|
+
t.column :security_token, 'CHAR(40)', :default => nil
|
13
|
+
t.column :token_expiry, :datetime, :default => nil
|
14
|
+
t.column :deleted, :boolean, :default => false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :<%= plural_name %>
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'models/<%= file_name %>_notify.rb'
|
2
|
+
|
3
|
+
ActionMailer::Base.class_eval {
|
4
|
+
@@inject_one_error = false
|
5
|
+
cattr_accessor :inject_one_error
|
6
|
+
|
7
|
+
private
|
8
|
+
def perform_delivery_test(mail)
|
9
|
+
if inject_one_error
|
10
|
+
ActionMailer::Base::inject_one_error = false
|
11
|
+
raise "Failed to send email" if raise_delivery_errors
|
12
|
+
else
|
13
|
+
deliveries << mail
|
14
|
+
end
|
15
|
+
end
|
16
|
+
}
|
data/templates/notify.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
class <%= class_name %>Notify < ActionMailer::Base
|
2
|
+
def signup(<%= singular_name %>, password, url=nil)
|
3
|
+
setup_email(<%= singular_name %>)
|
4
|
+
|
5
|
+
# Email header info
|
6
|
+
@subject += "Welcome to #{<%= class_name %>System::CONFIG[:app_name]}!"
|
7
|
+
|
8
|
+
# Email body substitutions
|
9
|
+
@body["name"] = "#{<%= singular_name %>.first_name} #{<%= singular_name %>.last_name}"
|
10
|
+
@body["login"] = <%= singular_name %>.login
|
11
|
+
@body["password"] = password
|
12
|
+
@body["url"] = url || <%= class_name %>System::CONFIG[:app_url].to_s
|
13
|
+
@body["app_name"] = <%= class_name %>System::CONFIG[:app_name].to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def forgot_password(<%= singular_name %>, url=nil)
|
17
|
+
setup_email(<%= singular_name %>)
|
18
|
+
|
19
|
+
# Email header info
|
20
|
+
@subject += "Forgotten password notification"
|
21
|
+
|
22
|
+
# Email body substitutions
|
23
|
+
@body["name"] = "#{<%= singular_name %>.first_name} #{<%= singular_name %>.last_name}"
|
24
|
+
@body["login"] = <%= singular_name %>.login
|
25
|
+
@body["url"] = url || <%= class_name %>System::CONFIG[:app_url].to_s
|
26
|
+
@body["app_name"] = <%= class_name %>System::CONFIG[:app_name].to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def change_password(<%= singular_name %>, password, url=nil)
|
30
|
+
setup_email(<%= singular_name %>)
|
31
|
+
|
32
|
+
# Email header info
|
33
|
+
@subject += "Changed password notification"
|
34
|
+
|
35
|
+
# Email body substitutions
|
36
|
+
@body["name"] = "#{<%= singular_name %>.first_name} #{<%= singular_name %>.last_name}"
|
37
|
+
@body["login"] = <%= singular_name %>.login
|
38
|
+
@body["password"] = password
|
39
|
+
@body["url"] = url || <%= class_name %>System::CONFIG[:app_url].to_s
|
40
|
+
@body["app_name"] = <%= class_name %>System::CONFIG[:app_name].to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def setup_email(<%= singular_name %>)
|
44
|
+
@recipients = "#{<%= singular_name %>.email}"
|
45
|
+
@from = <%= class_name %>System::CONFIG[:email_from].to_s
|
46
|
+
@subject = "[#{<%= class_name %>System::CONFIG[:app_name]}] "
|
47
|
+
@sent_on = Time.now
|
48
|
+
@headers['Content-Type'] = "text/plain; charset=#{<%= class_name %>System::CONFIG[:mail_charset]}; format=flowed"
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Dear <%%= @name %>,
|
2
|
+
|
3
|
+
At your request, <%%= @app_name %> has changed your password. If it was not at your request, then you should be aware that someone has access to your account and requested this change.
|
4
|
+
|
5
|
+
Your new login credentials are:
|
6
|
+
|
7
|
+
login: <%%= @login %>
|
8
|
+
password: <%%= @password %>
|
9
|
+
|
10
|
+
<%%= @url %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Dear <%%= @name %>,
|
2
|
+
|
3
|
+
At your request, <%%= @app_name %> has sent you the following URL so that you may reset your password. If it was not at your request, then you should be aware that someone has entered your email address as theirs in the forgotten password section of <%%= @app_name %>.
|
4
|
+
|
5
|
+
Please click on the following link to go to the change password page:
|
6
|
+
|
7
|
+
<a href="<%%= @url%>">Click me!</a>
|
8
|
+
|
9
|
+
It's advisable for you to change your password as soon as you login.
|
10
|
+
|
11
|
+
<%%= @url %>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Dear <%%= @name %>,
|
2
|
+
|
3
|
+
At your request, <%%= @app_name %> has marked your account for deletion. If it was not at your request, then you should be aware that someone has access to your account and requested this change.
|
4
|
+
|
5
|
+
The following link is provided for you to restore your deleted account. If you click on this link within the next <%%= @days %> days, your account will not be deleted. Otherwise, simply ignore this email and your account will be permanently deleted after that time.
|
6
|
+
|
7
|
+
<a href="<%%= @url%>">Click me!</a>
|
8
|
+
|
9
|
+
<%%= @url %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Welcome to <%%= @app_name %>, <%%= @name %>.
|
2
|
+
|
3
|
+
Your login credentials are:
|
4
|
+
|
5
|
+
login: <%%= @login %>
|
6
|
+
password: <%%= @password %>
|
7
|
+
|
8
|
+
Please click on the following link to confirm your registration:
|
9
|
+
|
10
|
+
<a href="<%%= @url%>">Click me!</a>
|
11
|
+
|
12
|
+
<%%= @url %>
|
@@ -0,0 +1,74 @@
|
|
1
|
+
body { background-color: #fff; color: #333; }
|
2
|
+
|
3
|
+
body, p, ol, ul, td {
|
4
|
+
font-family: verdana, arial, helvetica, sans-serif;
|
5
|
+
font-size: 13px;
|
6
|
+
line-height: 18px;
|
7
|
+
}
|
8
|
+
|
9
|
+
pre {
|
10
|
+
background-color: #eee;
|
11
|
+
padding: 10px;
|
12
|
+
font-size: 11px;
|
13
|
+
}
|
14
|
+
|
15
|
+
a { color: #000; }
|
16
|
+
a:visited { color: #666; }
|
17
|
+
a:hover { color: #fff; background-color:#000; }
|
18
|
+
|
19
|
+
.fieldWithErrors {
|
20
|
+
padding: 2px;
|
21
|
+
background-color: red;
|
22
|
+
display: table;
|
23
|
+
}
|
24
|
+
|
25
|
+
#errorExplanation {
|
26
|
+
width: 400px;
|
27
|
+
border: 2px solid red;
|
28
|
+
padding: 7px;
|
29
|
+
padding-bottom: 12px;
|
30
|
+
margin-bottom: 20px;
|
31
|
+
background-color: #f0f0f0;
|
32
|
+
}
|
33
|
+
|
34
|
+
#errorExplanation h2 {
|
35
|
+
text-align: left;
|
36
|
+
font-weight: bold;
|
37
|
+
padding: 5px 5px 5px 15px;
|
38
|
+
font-size: 12px;
|
39
|
+
margin: -7px;
|
40
|
+
background-color: #c00;
|
41
|
+
color: #fff;
|
42
|
+
}
|
43
|
+
|
44
|
+
#errorExplanation p {
|
45
|
+
color: #333;
|
46
|
+
margin-bottom: 0;
|
47
|
+
padding: 5px;
|
48
|
+
}
|
49
|
+
|
50
|
+
#errorExplanation ul li {
|
51
|
+
font-size: 12px;
|
52
|
+
list-style: square;
|
53
|
+
}
|
54
|
+
|
55
|
+
div.uploadStatus {
|
56
|
+
margin: 5px;
|
57
|
+
}
|
58
|
+
|
59
|
+
div.progressBar {
|
60
|
+
margin: 5px;
|
61
|
+
}
|
62
|
+
|
63
|
+
div.progressBar div.border {
|
64
|
+
background-color: #fff;
|
65
|
+
border: 1px solid grey;
|
66
|
+
width: 100%;
|
67
|
+
}
|
68
|
+
|
69
|
+
div.progressBar div.background {
|
70
|
+
background-color: #333;
|
71
|
+
height: 18px;
|
72
|
+
width: 0%;
|
73
|
+
}
|
74
|
+
|
data/templates/user.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
# this model expects a certain database layout and its based on the name/login pattern.
|
4
|
+
class <%= class_name %> < ActiveRecord::Base
|
5
|
+
attr_accessor :password_needs_confirmation
|
6
|
+
|
7
|
+
after_save '@password_needs_confirmation = false'
|
8
|
+
after_validation :crypt_password
|
9
|
+
|
10
|
+
validates_presence_of :login, :on => :create
|
11
|
+
validates_length_of :login, :within => 3..40, :on => :create
|
12
|
+
validates_uniqueness_of :login, :on => :create
|
13
|
+
validates_uniqueness_of :email, :on => :create
|
14
|
+
|
15
|
+
validates_presence_of :password, :if => :validate_password?
|
16
|
+
validates_confirmation_of :password, :if => :validate_password?
|
17
|
+
validates_length_of :password, { :minimum => 5, :if => :validate_password? }
|
18
|
+
validates_length_of :password, { :maximum => 40, :if => :validate_password? }
|
19
|
+
|
20
|
+
def initialize(attributes = nil)
|
21
|
+
super
|
22
|
+
@password_needs_confirmation = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.authenticate(login, pass)
|
26
|
+
u = find_first(["login = ? AND verified = TRUE AND deleted = FALSE", login])
|
27
|
+
return nil if u.nil?
|
28
|
+
find_first(["login = ? AND salted_password = ? AND verified = TRUE", login, salted_password(u.salt, hashed(pass))])
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.authenticate_by_token(id, token)
|
32
|
+
# Allow logins for deleted accounts, but only via this method (and
|
33
|
+
# not the regular authenticate call)
|
34
|
+
logger.info "Attempting authorization of #{id} with #{token}"
|
35
|
+
u = find_first(["id = ? AND security_token = ?", id, token])
|
36
|
+
if u
|
37
|
+
logger.info "Authenticated by token: #{u.inspect}"
|
38
|
+
else
|
39
|
+
logger.info "Not authenticated" if u.nil?
|
40
|
+
end
|
41
|
+
return nil if (u.nil? or u.token_expired?)
|
42
|
+
u.update_attribute :verified, true
|
43
|
+
return u
|
44
|
+
end
|
45
|
+
|
46
|
+
def token_expired?
|
47
|
+
self.security_token and self.token_expiry and (Clock.now > self.token_expiry)
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_security_token
|
51
|
+
if self.security_token.nil? or self.token_expiry.nil? or (Clock.now.to_i + token_lifetime / 2) >= self.token_expiry.to_i
|
52
|
+
token = new_security_token
|
53
|
+
return token
|
54
|
+
else
|
55
|
+
return self.security_token
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def change_password(pass, confirm = nil)
|
60
|
+
self.password = pass
|
61
|
+
self.password_confirmation = confirm.nil? ? pass : confirm
|
62
|
+
@password_needs_confirmation = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def token_lifetime
|
66
|
+
<%= class_name %>System::CONFIG[:security_token_life_hours] * 60 * 60
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
attr_accessor :password, :password_confirmation
|
73
|
+
|
74
|
+
def validate_password?
|
75
|
+
@password_needs_confirmation
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.hashed(str)
|
79
|
+
return Digest::SHA1.hexdigest("change-me--#{str}--")[0..39]
|
80
|
+
end
|
81
|
+
|
82
|
+
def crypt_password
|
83
|
+
if @password_needs_confirmation
|
84
|
+
write_attribute("salt", self.class.hashed("salt-#{Clock.now}"))
|
85
|
+
write_attribute("salted_password", self.class.salted_password(salt, self.class.hashed(@password)))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def new_security_token
|
90
|
+
expiry = Time.at(Clock.now.to_i + token_lifetime)
|
91
|
+
write_attribute('security_token', self.class.hashed(self.salted_password + Clock.now.to_i.to_s + rand.to_s))
|
92
|
+
write_attribute('token_expiry', expiry)
|
93
|
+
update_without_callbacks
|
94
|
+
return self.security_token
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.salted_password(salt, hashed_password)
|
98
|
+
hashed(salt + hashed_password)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
CREATE TABLE <%= plural_name %> (
|
2
|
+
id <%%= @pk %>,
|
3
|
+
login VARCHAR(80) NOT NULL,
|
4
|
+
salted_password VARCHAR(40) NOT NULL,
|
5
|
+
email VARCHAR(60) NOT NULL,
|
6
|
+
first_name VARCHAR(40) default NULL,
|
7
|
+
last_name VARCHAR(40) default NULL,
|
8
|
+
salt CHAR(40) NOT NULL,
|
9
|
+
verified INT default 0,
|
10
|
+
role VARCHAR(40) default NULL,
|
11
|
+
security_token CHAR(40) default NULL,
|
12
|
+
token_expiry <%%= @datetime %> default NULL,
|
13
|
+
created_at <%%= @datetime %> default NULL,
|
14
|
+
updated_at <%%= @datetime %> default NULL,
|
15
|
+
logged_in_at <%%= @datetime %> default NULL,
|
16
|
+
deleted INT default 0,
|
17
|
+
delete_after <%%= @datetime %> default NULL
|
18
|
+
) <%%= @options %>;
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class <%= class_name %>Test < Test::Unit::TestCase
|
4
|
+
fixtures :<%= plural_name %>
|
5
|
+
self.use_transactional_fixtures = false
|
6
|
+
|
7
|
+
def test_authenticate
|
8
|
+
assert_equal <%= plural_name %>(:tesla), <%= class_name %>.authenticate(<%= plural_name %>(:tesla).login, "atest")
|
9
|
+
assert_nil <%= class_name %>.authenticate("nontesla", "atest")
|
10
|
+
assert_nil <%= class_name %>.authenticate(<%= plural_name %>(:tesla), "wrong password")
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_authenticate_by_token
|
14
|
+
<%= singular_name %> = <%= plural_name %>(:unverified_<%= singular_name %>)
|
15
|
+
assert_equal <%= singular_name %>, <%= class_name %>.authenticate_by_token(<%= singular_name %>.id, <%= singular_name %>.security_token)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_authenticate_by_token__fails_if_expired
|
19
|
+
<%= singular_name %> = <%= plural_name %>(:unverified_<%= singular_name %>)
|
20
|
+
Clock.time = Clock.now + 2.days
|
21
|
+
assert_nil <%= class_name %>.authenticate_by_token(<%= singular_name %>.id, <%= singular_name %>.security_token)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_authenticate_by_token__fails_if_bad_token
|
25
|
+
<%= singular_name %> = <%= plural_name %>(:unverified_<%= singular_name %>)
|
26
|
+
assert_nil <%= class_name %>.authenticate_by_token(<%= singular_name %>.id, 'bad_token')
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_authenticate_by_token__fails_if_bad_id
|
30
|
+
<%= singular_name %> = <%= plural_name %>(:unverified_<%= singular_name %>)
|
31
|
+
assert_nil <%= class_name %>.authenticate_by_token(-1, <%= singular_name %>.security_token)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_change_password
|
35
|
+
<%= singular_name %> = <%= plural_name %>(:long_<%= singular_name %>)
|
36
|
+
<%= singular_name %>.change_password("a new password")
|
37
|
+
<%= singular_name %>.save
|
38
|
+
assert_equal <%= singular_name %>, <%= class_name %>.authenticate(<%= singular_name %>.login, "a new password")
|
39
|
+
assert_nil <%= class_name %>.authenticate(<%= singular_name %>.login, "alongtest")
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_generate_security_token
|
43
|
+
<%= singular_name %> = <%= class_name %>.new :login => '<%= singular_name %>', :email => '<%= singular_name %>@example.com', :salt => 'salt', :salted_password => 'tlas'
|
44
|
+
<%= singular_name %>.save
|
45
|
+
token = <%= singular_name %>.generate_security_token
|
46
|
+
assert_not_nil token
|
47
|
+
<%= singular_name %>.reload
|
48
|
+
assert_equal token, <%= singular_name %>.security_token
|
49
|
+
assert_equal (Clock.now + <%= singular_name %>.token_lifetime).to_i, <%= singular_name %>.token_expiry.to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_generate_security_token__reuses_token_when_not_stale
|
53
|
+
<%= singular_name %> = <%= plural_name %>(:unverified_<%= singular_name %>)
|
54
|
+
Clock.time = Clock.now + <%= singular_name %>.token_lifetime/2 - 1
|
55
|
+
assert_equal <%= singular_name %>.security_token, <%= singular_name %>.generate_security_token
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_generate_security_token__generates_new_token_when_getting_stale
|
59
|
+
<%= singular_name %> = <%= plural_name %>(:unverified_<%= singular_name %>)
|
60
|
+
Clock.time = Clock.now + <%= singular_name %>.token_lifetime/2
|
61
|
+
assert_not_equal <%= singular_name %>.security_token, <%= singular_name %>.generate_security_token
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_change_password__disallowed_passwords
|
65
|
+
u = <%= class_name %>.new
|
66
|
+
u.login = "test_<%= singular_name %>"
|
67
|
+
u.email = 'disallowed_password@example.com'
|
68
|
+
|
69
|
+
u.change_password("tiny")
|
70
|
+
assert !u.save
|
71
|
+
assert u.errors.invalid?('password')
|
72
|
+
|
73
|
+
u.change_password("hugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehuge")
|
74
|
+
assert !u.save
|
75
|
+
assert u.errors.invalid?('password')
|
76
|
+
|
77
|
+
u.change_password("")
|
78
|
+
assert !u.save
|
79
|
+
assert u.errors.invalid?('password')
|
80
|
+
|
81
|
+
u.change_password("a_s3cure_p4ssword")
|
82
|
+
assert u.save
|
83
|
+
assert u.errors.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_validates_login
|
87
|
+
u = <%= class_name %>.new
|
88
|
+
u.change_password("teslas_secure_password")
|
89
|
+
u.email = 'bad_login_tesla@example.com'
|
90
|
+
|
91
|
+
u.login = "x"
|
92
|
+
assert !u.save
|
93
|
+
assert u.errors.invalid?(:login)
|
94
|
+
|
95
|
+
u.login = "hugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahugeteslahug"
|
96
|
+
assert !u.save
|
97
|
+
assert u.errors.invalid?(:login)
|
98
|
+
|
99
|
+
u.login = ""
|
100
|
+
assert !u.save
|
101
|
+
assert u.errors.invalid?(:login)
|
102
|
+
|
103
|
+
u.login = "oktesla"
|
104
|
+
assert u.save
|
105
|
+
assert u.errors.empty?
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_create
|
110
|
+
u = <%= class_name %>.new
|
111
|
+
u.login = "nonexisting_<%= singular_name %>"
|
112
|
+
u.email = 'nonexisting_email@example.com'
|
113
|
+
u.change_password("password")
|
114
|
+
assert u.save
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_create__validates_unique_login
|
118
|
+
u = <%= class_name %>.new
|
119
|
+
u.login = <%= plural_name %>(:tesla).login
|
120
|
+
u.email = 'new@example.com'
|
121
|
+
u.change_password("password")
|
122
|
+
assert !u.save
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_create__validates_unique_email
|
126
|
+
u = <%= class_name %>.new
|
127
|
+
u.login = 'new_<%= singular_name %>'
|
128
|
+
u.email= <%= plural_name %>(:tesla).email
|
129
|
+
u.change_password("password")
|
130
|
+
assert !u.save
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|