attr_encryptor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +16 -0
- data/.rvmrc +47 -0
- data/MIT-LICENSE +20 -0
- data/README.md +291 -0
- data/Rakefile +22 -0
- data/attr_encryptor.gemspec +36 -0
- data/lib/attr_encryptor.rb +326 -0
- data/lib/attr_encryptor/adapters/active_record.rb +25 -0
- data/lib/attr_encryptor/adapters/data_mapper.rb +21 -0
- data/lib/attr_encryptor/adapters/sequel.rb +14 -0
- data/lib/attr_encryptor/version.rb +17 -0
- data/test/active_record_test.rb +98 -0
- data/test/attr_encrypted_test.rb +290 -0
- data/test/data_mapper_test.rb +52 -0
- data/test/debug_order.rb +41 -0
- data/test/sequel_test.rb +50 -0
- data/test/test_helper.rb +14 -0
- metadata +149 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
if defined?(ActiveRecord::Base)
|
2
|
+
module AttrEncryptor
|
3
|
+
module Adapters
|
4
|
+
module ActiveRecord
|
5
|
+
def self.extended(base) # :nodoc:
|
6
|
+
base.class_eval do
|
7
|
+
attr_encrypted_options[:encode] = true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
# Ensures the attribute methods for db fields have been defined before calling the original
|
14
|
+
# <tt>attr_encrypted</tt> method
|
15
|
+
def attr_encrypted(*attrs)
|
16
|
+
define_attribute_methods rescue nil
|
17
|
+
super
|
18
|
+
attrs.reject { |attr| attr.is_a?(Hash) }.each { |attr| alias_method "#{attr}_before_type_cast", attr }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ActiveRecord::Base.extend AttrEncryptor::Adapters::ActiveRecord
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
if defined?(DataMapper)
|
2
|
+
module AttrEncryptor
|
3
|
+
module Adapters
|
4
|
+
module DataMapper
|
5
|
+
def self.extended(base) # :nodoc:
|
6
|
+
class << base
|
7
|
+
alias_method :included_without_attr_encrypted, :included
|
8
|
+
alias_method :included, :included_with_attr_encrypted
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def included_with_attr_encrypted(base)
|
13
|
+
included_without_attr_encrypted(base)
|
14
|
+
base.attr_encrypted_options[:encode] = true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
DataMapper::Resource.extend AttrEncryptor::Adapters::DataMapper
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AttrEncryptor
|
2
|
+
# Contains information about this gem's version
|
3
|
+
module Version
|
4
|
+
MAJOR = 1
|
5
|
+
MINOR = 0
|
6
|
+
PATCH = 0
|
7
|
+
|
8
|
+
# Returns a version string by joining <tt>MAJOR</tt>, <tt>MINOR</tt>, and <tt>PATCH</tt> with <tt>'.'</tt>
|
9
|
+
#
|
10
|
+
# Example
|
11
|
+
#
|
12
|
+
# Version.string # '1.0.2'
|
13
|
+
def self.string
|
14
|
+
[MAJOR, MINOR, PATCH].join('.')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
|
4
|
+
|
5
|
+
def create_tables
|
6
|
+
silence_stream(STDOUT) do
|
7
|
+
ActiveRecord::Schema.define(:version => 1) do
|
8
|
+
create_table :people do |t|
|
9
|
+
t.string :encrypted_email
|
10
|
+
t.string :password
|
11
|
+
t.string :encrypted_credentials
|
12
|
+
t.binary :salt
|
13
|
+
t.string :encrypted_email_salt
|
14
|
+
t.string :encrypted_credentials_salt
|
15
|
+
t.string :encrypted_email_iv
|
16
|
+
t.string :encrypted_credentials_iv
|
17
|
+
end
|
18
|
+
create_table :accounts do |t|
|
19
|
+
t.string :encrypted_password
|
20
|
+
t.string :encrypted_password_iv
|
21
|
+
t.string :encrypted_password_salt
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# The table needs to exist before defining the class
|
28
|
+
create_tables
|
29
|
+
|
30
|
+
ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError)
|
31
|
+
|
32
|
+
class Person < ActiveRecord::Base
|
33
|
+
attr_encrypted :email, :key => "secret"
|
34
|
+
attr_encrypted :credentials, :key => Proc.new { |user| Encryptor.encrypt(:value => user.salt, :key => 'secret_key') }, :marshal => true
|
35
|
+
|
36
|
+
|
37
|
+
after_initialize :initialize_salt_and_credentials
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def initialize_salt_and_credentials
|
42
|
+
self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(5)).to_s)
|
43
|
+
self.credentials ||= { :username => 'example', :password => 'test' }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class PersonWithValidation < Person
|
48
|
+
validates_presence_of :email
|
49
|
+
end
|
50
|
+
|
51
|
+
class Account < ActiveRecord::Base
|
52
|
+
attr_accessor :key
|
53
|
+
attr_encrypted :password, :key => Proc.new {|account| account.key}
|
54
|
+
end
|
55
|
+
|
56
|
+
class ActiveRecordTest < Test::Unit::TestCase
|
57
|
+
|
58
|
+
def setup
|
59
|
+
ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
|
60
|
+
create_tables
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_should_encrypt_email
|
64
|
+
@person = Person.create :email => 'test@example.com'
|
65
|
+
assert_not_nil @person.encrypted_email
|
66
|
+
assert_not_equal @person.email, @person.encrypted_email
|
67
|
+
assert_equal @person.email, Person.find(:first).email
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_should_marshal_and_encrypt_credentials
|
71
|
+
@person = Person.create
|
72
|
+
assert_not_nil @person.encrypted_credentials
|
73
|
+
assert_not_equal @person.credentials, @person.encrypted_credentials
|
74
|
+
assert_equal @person.credentials, Person.find(:first).credentials
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_should_encode_by_default
|
78
|
+
assert Person.attr_encrypted_options[:encode]
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_should_validate_presence_of_email
|
82
|
+
@person = PersonWithValidation.new
|
83
|
+
assert !@person.valid?
|
84
|
+
assert !@person.errors[:email].empty? || @person.errors.on(:email)
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_should_encrypt_decrypt_with_iv
|
88
|
+
@person = Person.create :email => 'test@example.com'
|
89
|
+
@person2 = Person.find(@person.id)
|
90
|
+
assert_not_nil @person2.encrypted_email_iv
|
91
|
+
assert_equal 'test@example.com', @person2.email
|
92
|
+
end
|
93
|
+
|
94
|
+
def _test_should_create_an_account_regardless_of_arguments_order
|
95
|
+
Account.create!(:key => "secret", :password => "password")
|
96
|
+
Account.create!(:password => "password" , :key => "secret")
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class SillyEncryptor
|
4
|
+
def self.silly_encrypt(options)
|
5
|
+
(options[:value] + options[:some_arg]).reverse
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.silly_decrypt(options)
|
9
|
+
options[:value].reverse.gsub(/#{options[:some_arg]}$/, '')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class User
|
14
|
+
self.attr_encrypted_options[:key] = Proc.new { |user| user.class.to_s } # default key
|
15
|
+
|
16
|
+
attr_encrypted :email, :without_encoding, :key => 'secret key'
|
17
|
+
attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test'
|
18
|
+
attr_encrypted :ssn, :key => :salt, :attribute => 'ssn_encrypted'
|
19
|
+
attr_encrypted :credit_card, :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test'
|
20
|
+
attr_encrypted :with_encoding, :key => 'secret key', :encode => true
|
21
|
+
attr_encrypted :with_custom_encoding, :key => 'secret key', :encode => 'm'
|
22
|
+
attr_encrypted :with_marshaling, :key => 'secret key', :marshal => true
|
23
|
+
attr_encrypted :with_true_if, :key => 'secret key', :if => true
|
24
|
+
attr_encrypted :with_false_if, :key => 'secret key', :if => false
|
25
|
+
attr_encrypted :with_true_unless, :key => 'secret key', :unless => true
|
26
|
+
attr_encrypted :with_false_unless, :key => 'secret key', :unless => false
|
27
|
+
attr_encrypted :with_if_changed, :key => 'secret key', :if => :should_encrypt
|
28
|
+
|
29
|
+
attr_encryptor :aliased, :key => 'secret_key'
|
30
|
+
|
31
|
+
attr_accessor :salt
|
32
|
+
attr_accessor :should_encrypt
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
self.salt = Time.now.to_i.to_s
|
36
|
+
self.should_encrypt = true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Admin < User
|
41
|
+
attr_encrypted :testing
|
42
|
+
end
|
43
|
+
|
44
|
+
class SomeOtherClass
|
45
|
+
def self.call(object)
|
46
|
+
object.class
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class AttrEncryptorTest < Test::Unit::TestCase
|
51
|
+
|
52
|
+
def test_should_store_email_in_encrypted_attributes
|
53
|
+
assert User.encrypted_attributes.include?(:email)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_should_not_store_salt_in_encrypted_attributes
|
57
|
+
assert !User.encrypted_attributes.include?(:salt)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_attr_encrypted_should_return_true_for_email
|
61
|
+
assert User.attr_encrypted?('email')
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line
|
65
|
+
assert_not_equal User.encrypted_attributes[:email][:attribute], User.encrypted_attributes[:without_encoding][:attribute]
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_attr_encrypted_should_return_false_for_salt
|
69
|
+
assert !User.attr_encrypted?('salt')
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_should_generate_an_encrypted_attribute
|
73
|
+
assert User.new.respond_to?(:encrypted_email)
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix
|
77
|
+
assert User.new.respond_to?(:crypted_password_test)
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_should_generate_an_encrypted_attribute_with_the_attribute_option
|
81
|
+
assert User.new.respond_to?(:ssn_encrypted)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_should_not_encrypt_nil_value
|
85
|
+
assert_nil User.encrypt_email(nil)
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_should_not_encrypt_empty_string
|
89
|
+
assert_equal '', User.encrypt_email('')
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_should_encrypt_email
|
93
|
+
assert_not_nil User.encrypt_email('test@example.com')
|
94
|
+
assert_not_equal 'test@example.com', User.encrypt_email('test@example.com')
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_should_encrypt_email_when_modifying_the_attr_writer
|
98
|
+
@user = User.new
|
99
|
+
assert_nil @user.encrypted_email
|
100
|
+
@user.email = 'test@example.com'
|
101
|
+
assert_not_nil @user.encrypted_email
|
102
|
+
assert_equal User.encrypt_email('test@example.com'), @user.encrypted_email
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_should_not_decrypt_nil_value
|
106
|
+
assert_nil User.decrypt_email(nil)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_should_not_decrypt_empty_string
|
110
|
+
assert_equal '', User.decrypt_email('')
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_should_decrypt_email
|
114
|
+
encrypted_email = User.encrypt_email('test@example.com')
|
115
|
+
assert_not_equal 'test@test.com', encrypted_email
|
116
|
+
assert_equal 'test@example.com', User.decrypt_email(encrypted_email)
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_should_decrypt_email_when_reading
|
120
|
+
@user = User.new
|
121
|
+
assert_nil @user.email
|
122
|
+
@user.encrypted_email = User.encrypt_email('test@example.com')
|
123
|
+
assert_equal 'test@example.com', @user.email
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_should_encrypt_with_encoding
|
127
|
+
assert_equal User.encrypt_with_encoding('test'), [User.encrypt_without_encoding('test')].pack('m')
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_should_decrypt_with_encoding
|
131
|
+
encrypted = User.encrypt_with_encoding('test')
|
132
|
+
assert_equal 'test', User.decrypt_with_encoding(encrypted)
|
133
|
+
assert_equal User.decrypt_with_encoding(encrypted), User.decrypt_without_encoding(encrypted.unpack('m').first)
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_should_encrypt_with_custom_encoding
|
137
|
+
assert_equal User.encrypt_with_encoding('test'), [User.encrypt_without_encoding('test')].pack('m')
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_should_decrypt_with_custom_encoding
|
141
|
+
encrypted = User.encrypt_with_encoding('test')
|
142
|
+
assert_equal 'test', User.decrypt_with_encoding(encrypted)
|
143
|
+
assert_equal User.decrypt_with_encoding(encrypted), User.decrypt_without_encoding(encrypted.unpack('m').first)
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_should_encrypt_with_marshaling
|
147
|
+
@user = User.new
|
148
|
+
@user.with_marshaling = [1, 2, 3]
|
149
|
+
assert_not_nil @user.encrypted_with_marshaling
|
150
|
+
assert_equal User.encrypt_with_marshaling([1, 2, 3]), @user.encrypted_with_marshaling
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_should_decrypt_with_marshaling
|
154
|
+
encrypted = User.encrypt_with_marshaling([1, 2, 3])
|
155
|
+
@user = User.new
|
156
|
+
assert_nil @user.with_marshaling
|
157
|
+
@user.encrypted_with_marshaling = encrypted
|
158
|
+
assert_equal [1, 2, 3], @user.with_marshaling
|
159
|
+
end
|
160
|
+
|
161
|
+
def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments
|
162
|
+
assert_equal SillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), User.encrypt_credit_card('testing')
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_should_evaluate_a_key_passed_as_a_symbol
|
166
|
+
@user = User.new
|
167
|
+
assert_nil @user.ssn_encrypted
|
168
|
+
@user.ssn = 'testing'
|
169
|
+
assert_not_nil @user.ssn_encrypted
|
170
|
+
assert_equal Encryptor.encrypt(:value => 'testing', :key => @user.salt, :iv => @user.ssn_encrypted_iv.unpack("m").first, :salt => Time.now.to_i.to_s), @user.ssn_encrypted
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_should_evaluate_a_key_passed_as_a_proc
|
174
|
+
@user = User.new
|
175
|
+
assert_nil @user.crypted_password_test
|
176
|
+
@user.password = 'testing'
|
177
|
+
assert_not_nil @user.crypted_password_test
|
178
|
+
assert_equal Encryptor.encrypt(:value => 'testing', :key => 'User', :iv => @user.crypted_password_test_iv.unpack("m").first, :salt => Time.now.to_i.to_s), @user.crypted_password_test
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_should_use_options_found_in_the_attr_encrypted_options_attribute
|
182
|
+
@user = User.new
|
183
|
+
assert_nil @user.crypted_password_test
|
184
|
+
@user.password = 'testing'
|
185
|
+
assert_not_nil @user.crypted_password_test
|
186
|
+
assert_equal Encryptor.encrypt(:value => 'testing', :key => 'User', :iv => @user.crypted_password_test_iv.unpack("m").first, :salt => Time.now.to_i.to_s), @user.crypted_password_test
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_should_inherit_encrypted_attributes
|
190
|
+
assert_equal [User.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, Admin.encrypted_attributes.keys.collect { |key| key.to_s }.sort
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_should_inherit_attr_encrypted_options
|
194
|
+
assert !User.attr_encrypted_options.empty?
|
195
|
+
assert_equal User.attr_encrypted_options, Admin.attr_encrypted_options
|
196
|
+
end
|
197
|
+
|
198
|
+
def test_should_not_inherit_unrelated_attributes
|
199
|
+
assert SomeOtherClass.attr_encrypted_options.empty?
|
200
|
+
assert SomeOtherClass.encrypted_attributes.empty?
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_should_evaluate_a_symbol_option
|
204
|
+
assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, :class)
|
205
|
+
end
|
206
|
+
|
207
|
+
def test_should_evaluate_a_proc_option
|
208
|
+
assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, proc { |object| object.class })
|
209
|
+
end
|
210
|
+
|
211
|
+
def test_should_evaluate_a_lambda_option
|
212
|
+
assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, lambda { |object| object.class })
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_should_evaluate_a_method_option
|
216
|
+
assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, SomeOtherClass.method(:call))
|
217
|
+
end
|
218
|
+
|
219
|
+
def test_should_return_a_string_option
|
220
|
+
assert_equal 'Object', Object.new.send(:evaluate_attr_encrypted_option, 'Object')
|
221
|
+
end
|
222
|
+
|
223
|
+
def test_should_encrypt_with_true_if
|
224
|
+
@user = User.new
|
225
|
+
assert_nil @user.encrypted_with_true_if
|
226
|
+
@user.with_true_if = 'testing'
|
227
|
+
assert_not_nil @user.encrypted_with_true_if
|
228
|
+
assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key', :iv => @user.encrypted_with_true_if_iv.unpack("m").first, :salt => Time.now.to_i.to_s), @user.encrypted_with_true_if
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_should_not_encrypt_with_false_if
|
232
|
+
@user = User.new
|
233
|
+
assert_nil @user.encrypted_with_false_if
|
234
|
+
@user.with_false_if = 'testing'
|
235
|
+
assert_not_nil @user.encrypted_with_false_if
|
236
|
+
assert_equal 'testing', @user.encrypted_with_false_if
|
237
|
+
end
|
238
|
+
|
239
|
+
def test_should_encrypt_with_false_unless
|
240
|
+
@user = User.new
|
241
|
+
assert_nil @user.encrypted_with_false_unless
|
242
|
+
@user.with_false_unless = 'testing'
|
243
|
+
assert_not_nil @user.encrypted_with_false_unless
|
244
|
+
assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key', :iv => @user.encrypted_with_false_unless_iv.unpack("m").first, :salt => Time.now.to_i.to_s,), @user.encrypted_with_false_unless
|
245
|
+
end
|
246
|
+
|
247
|
+
def test_should_not_encrypt_with_true_unless
|
248
|
+
@user = User.new
|
249
|
+
assert_nil @user.encrypted_with_true_unless
|
250
|
+
@user.with_true_unless = 'testing'
|
251
|
+
assert_not_nil @user.encrypted_with_true_unless
|
252
|
+
assert_equal 'testing', @user.encrypted_with_true_unless
|
253
|
+
end
|
254
|
+
|
255
|
+
def test_should_work_with_aliased_attr_encryptor
|
256
|
+
assert User.encrypted_attributes.include?(:aliased)
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_should_always_reset_options
|
260
|
+
@user = User.new
|
261
|
+
@user.with_if_changed = "encrypt_stuff"
|
262
|
+
@user.stubs(:instance_variable_get).returns(nil)
|
263
|
+
@user.stubs(:instance_variable_set).raises("BadStuff")
|
264
|
+
assert_raise RuntimeError do
|
265
|
+
@user.with_if_changed
|
266
|
+
end
|
267
|
+
|
268
|
+
@user = User.new
|
269
|
+
@user.should_encrypt = false
|
270
|
+
@user.with_if_changed = "not_encrypted_stuff"
|
271
|
+
assert_equal "not_encrypted_stuff", @user.with_if_changed
|
272
|
+
assert_equal "not_encrypted_stuff", @user.encrypted_with_if_changed
|
273
|
+
end
|
274
|
+
|
275
|
+
def test_should_cast_values_as_strings_before_encrypting
|
276
|
+
string_encrypted_email = User.encrypt_email('3')
|
277
|
+
assert_equal string_encrypted_email, User.encrypt_email(3)
|
278
|
+
assert_equal '3', User.decrypt_email(string_encrypted_email)
|
279
|
+
end
|
280
|
+
|
281
|
+
def test_should_create_query_accessor
|
282
|
+
@user = User.new
|
283
|
+
assert !@user.email?
|
284
|
+
@user.email = ''
|
285
|
+
assert !@user.email?
|
286
|
+
@user.email = 'test@example.com'
|
287
|
+
assert @user.email?
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|