omniauth-identity 3.0.4 → 3.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +87 -13
- data/lib/omniauth-identity/version.rb +1 -1
- data/lib/omniauth/identity.rb +1 -1
- data/lib/omniauth/identity/model.rb +92 -29
- data/lib/omniauth/identity/models/active_record.rb +5 -2
- data/lib/omniauth/identity/models/couch_potato.rb +9 -1
- data/lib/omniauth/identity/models/mongoid.rb +3 -0
- data/lib/omniauth/identity/models/{no_brainer.rb → nobrainer.rb} +3 -1
- data/lib/omniauth/identity/models/sequel.rb +16 -5
- data/lib/omniauth/identity/secure_password.rb +98 -37
- data/lib/omniauth/strategies/identity.rb +47 -23
- data/spec/omniauth/identity/model_spec.rb +19 -99
- data/spec/omniauth/identity/models/active_record_spec.rb +20 -10
- data/spec/omniauth/identity/models/sequel_spec.rb +31 -15
- data/spec/omniauth/strategies/identity_spec.rb +124 -5
- data/spec/spec_helper.rb +16 -5
- data/spec/support/shared_contexts/instance_with_instance_methods.rb +89 -0
- data/spec/support/shared_contexts/model_with_class_methods.rb +29 -0
- data/spec/support/shared_contexts/persistable_model.rb +24 -0
- metadata +17 -65
- data/spec/omniauth/identity/models/couch_potato_spec.rb +0 -21
- data/spec/omniauth/identity/models/mongoid_spec.rb +0 -28
- data/spec/omniauth/identity/models/no_brainer_spec.rb +0 -17
@@ -5,7 +5,9 @@ require 'nobrainer'
|
|
5
5
|
module OmniAuth
|
6
6
|
module Identity
|
7
7
|
module Models
|
8
|
-
#
|
8
|
+
# NoBrainer is an ORM adapter for RethinkDB:
|
9
|
+
# http://nobrainer.io/
|
10
|
+
# NOTE: NoBrainer is based on ActiveModel.
|
9
11
|
module NoBrainer
|
10
12
|
def self.included(base)
|
11
13
|
base.class_eval do
|
@@ -1,11 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'sequel'
|
4
4
|
|
5
5
|
module OmniAuth
|
6
6
|
module Identity
|
7
7
|
module Models
|
8
|
-
#
|
8
|
+
# Sequel is an ORM adapter for the following databases:
|
9
|
+
# ADO, Amalgalite, IBM_DB, JDBC, MySQL, Mysql2, ODBC, Oracle, PostgreSQL, SQLAnywhere, SQLite3, and TinyTDS
|
10
|
+
# The homepage is: http://sequel.jeremyevans.net/
|
11
|
+
# NOTE: Sequel is *not* based on ActiveModel, but supports the API we need, except for `persisted?`:
|
12
|
+
# * create
|
13
|
+
# * save
|
9
14
|
module Sequel
|
10
15
|
def self.included(base)
|
11
16
|
base.class_eval do
|
@@ -14,13 +19,11 @@ module OmniAuth
|
|
14
19
|
# plugin :validation_helpers
|
15
20
|
plugin :validation_class_methods
|
16
21
|
|
17
|
-
include OmniAuth::Identity::Model
|
22
|
+
include ::OmniAuth::Identity::Model
|
18
23
|
include ::OmniAuth::Identity::SecurePassword
|
19
24
|
|
20
25
|
has_secure_password
|
21
26
|
|
22
|
-
alias_method :persisted?, :valid?
|
23
|
-
|
24
27
|
def self.auth_key=(key)
|
25
28
|
super
|
26
29
|
validates_uniqueness_of :key, case_sensitive: false
|
@@ -29,6 +32,14 @@ module OmniAuth
|
|
29
32
|
def self.locate(search_hash)
|
30
33
|
where(search_hash).first
|
31
34
|
end
|
35
|
+
|
36
|
+
def persisted?
|
37
|
+
exists?
|
38
|
+
end
|
39
|
+
|
40
|
+
def save
|
41
|
+
super
|
42
|
+
end
|
32
43
|
end
|
33
44
|
end
|
34
45
|
end
|
@@ -4,7 +4,7 @@ require 'bcrypt'
|
|
4
4
|
|
5
5
|
module OmniAuth
|
6
6
|
module Identity
|
7
|
-
# This is
|
7
|
+
# This is lightly edited from Rails 6.1 code and is used if
|
8
8
|
# the version of ActiveModel that's being used does not
|
9
9
|
# include SecurePassword. The only difference is that instead of
|
10
10
|
# using ActiveSupport::Concern, it checks to see if there is already
|
@@ -14,63 +14,124 @@ module OmniAuth
|
|
14
14
|
base.extend ClassMethods unless base.respond_to?(:has_secure_password)
|
15
15
|
end
|
16
16
|
|
17
|
+
# BCrypt hash function can handle maximum 72 bytes, and if we pass
|
18
|
+
# password of length more than 72 bytes it ignores extra characters.
|
19
|
+
# Hence need to put a restriction on password length.
|
20
|
+
MAX_PASSWORD_LENGTH_ALLOWED = 72
|
21
|
+
|
22
|
+
class << self
|
23
|
+
attr_accessor :min_cost # :nodoc:
|
24
|
+
end
|
25
|
+
self.min_cost = false
|
26
|
+
|
17
27
|
module ClassMethods
|
18
28
|
# Adds methods to set and authenticate against a BCrypt password.
|
19
|
-
# This mechanism requires you to have a
|
29
|
+
# This mechanism requires you to have a +XXX_digest+ attribute.
|
30
|
+
# Where +XXX+ is the attribute name of your desired password.
|
31
|
+
#
|
32
|
+
# The following validations are added automatically:
|
33
|
+
# * Password must be present on creation
|
34
|
+
# * Password length should be less than or equal to 72 bytes
|
35
|
+
# * Confirmation of password (using a +XXX_confirmation+ attribute)
|
20
36
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
37
|
+
# If confirmation validation is not needed, simply leave out the
|
38
|
+
# value for +XXX_confirmation+ (i.e. don't provide a form field for
|
39
|
+
# it). When this attribute has a +nil+ value, the validation will not be
|
40
|
+
# triggered.
|
41
|
+
#
|
42
|
+
# For further customizability, it is possible to suppress the default
|
43
|
+
# validations by passing <tt>validations: false</tt> as an argument.
|
44
|
+
#
|
45
|
+
# Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
|
46
|
+
#
|
47
|
+
# gem 'bcrypt', '~> 3.1.7'
|
24
48
|
#
|
25
49
|
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
|
26
50
|
#
|
27
|
-
# # Schema: User(name:string, password_digest:string)
|
51
|
+
# # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
|
28
52
|
# class User < ActiveRecord::Base
|
29
53
|
# has_secure_password
|
54
|
+
# has_secure_password :recovery_password, validations: false
|
30
55
|
# end
|
31
56
|
#
|
32
|
-
# user = User.new(:
|
33
|
-
# user.save
|
34
|
-
# user.password =
|
35
|
-
# user.save
|
36
|
-
# user.password_confirmation =
|
37
|
-
# user.save
|
38
|
-
# user.
|
39
|
-
# user.
|
40
|
-
#
|
41
|
-
#
|
42
|
-
|
43
|
-
|
57
|
+
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
|
58
|
+
# user.save # => false, password required
|
59
|
+
# user.password = 'mUc3m00RsqyRe'
|
60
|
+
# user.save # => false, confirmation doesn't match
|
61
|
+
# user.password_confirmation = 'mUc3m00RsqyRe'
|
62
|
+
# user.save # => true
|
63
|
+
# user.recovery_password = "42password"
|
64
|
+
# user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
|
65
|
+
# user.save # => true
|
66
|
+
# user.authenticate('notright') # => false
|
67
|
+
# user.authenticate('mUc3m00RsqyRe') # => user
|
68
|
+
# user.authenticate_recovery_password('42password') # => user
|
69
|
+
# User.find_by(name: 'david')&.authenticate('notright') # => false
|
70
|
+
# User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
|
71
|
+
def has_secure_password(attribute = :password, validations: true)
|
72
|
+
# Load bcrypt gem only when has_secure_password is used.
|
73
|
+
# This is to avoid ActiveModel (and by extension the entire framework)
|
74
|
+
# being dependent on a binary library.
|
75
|
+
begin
|
76
|
+
require 'bcrypt'
|
77
|
+
rescue LoadError
|
78
|
+
warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
|
79
|
+
raise
|
80
|
+
end
|
44
81
|
|
45
|
-
|
46
|
-
validates_presence_of :password_digest
|
82
|
+
include InstanceMethodsOnActivation.new(attribute)
|
47
83
|
|
48
|
-
|
84
|
+
if validations
|
85
|
+
include ActiveModel::Validations
|
49
86
|
|
50
|
-
|
51
|
-
|
52
|
-
|
87
|
+
# This ensures the model has a password by checking whether the password_digest
|
88
|
+
# is present, so that this works with both new and existing records. However,
|
89
|
+
# when there is an error, the message is added to the password attribute instead
|
90
|
+
# so that the error message will make sense to the end-user.
|
91
|
+
validate do |record|
|
92
|
+
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
|
53
93
|
end
|
94
|
+
|
95
|
+
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
|
96
|
+
validates_confirmation_of attribute, allow_blank: true
|
54
97
|
end
|
55
98
|
end
|
56
99
|
end
|
57
100
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
101
|
+
class InstanceMethodsOnActivation < Module
|
102
|
+
def initialize(attribute)
|
103
|
+
attr_reader attribute
|
104
|
+
|
105
|
+
define_method("#{attribute}=") do |unencrypted_password|
|
106
|
+
if unencrypted_password.nil?
|
107
|
+
public_send("#{attribute}_digest=", nil)
|
108
|
+
elsif !unencrypted_password.empty?
|
109
|
+
instance_variable_set("@#{attribute}", unencrypted_password)
|
110
|
+
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
111
|
+
public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
|
112
|
+
end
|
65
113
|
end
|
66
|
-
end
|
67
114
|
|
68
|
-
|
69
|
-
|
70
|
-
@password = unencrypted_password
|
71
|
-
if unencrypted_password && !unencrypted_password.empty?
|
72
|
-
self.password_digest = BCrypt::Password.create(unencrypted_password)
|
115
|
+
define_method("#{attribute}_confirmation=") do |unencrypted_password|
|
116
|
+
instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
|
73
117
|
end
|
118
|
+
|
119
|
+
# Returns +self+ if the password is correct, otherwise +false+.
|
120
|
+
#
|
121
|
+
# class User < ActiveRecord::Base
|
122
|
+
# has_secure_password validations: false
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
|
126
|
+
# user.save
|
127
|
+
# user.authenticate_password('notright') # => false
|
128
|
+
# user.authenticate_password('mUc3m00RsqyRe') # => user
|
129
|
+
define_method("authenticate_#{attribute}") do |unencrypted_password|
|
130
|
+
attribute_digest = public_send("#{attribute}_digest")
|
131
|
+
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
|
132
|
+
end
|
133
|
+
|
134
|
+
alias_method :authenticate, :authenticate_password if attribute == :password
|
74
135
|
end
|
75
136
|
end
|
76
137
|
end
|
@@ -6,8 +6,8 @@ module OmniAuth
|
|
6
6
|
# user authentication using the same process flow that you
|
7
7
|
# use for external OmniAuth providers.
|
8
8
|
class Identity
|
9
|
+
DEFAULT_REGISTRATION_FIELDS = %i[password password_confirmation].freeze
|
9
10
|
include OmniAuth::Strategy
|
10
|
-
|
11
11
|
option :fields, %i[name email]
|
12
12
|
|
13
13
|
# Primary Feature Switches:
|
@@ -20,6 +20,11 @@ module OmniAuth
|
|
20
20
|
option :on_registration, nil # See #registration_phase
|
21
21
|
option :on_failed_registration, nil # See #registration_phase
|
22
22
|
option :locate_conditions, ->(req) { { model.auth_key => req['auth_key'] } }
|
23
|
+
option :create_identity_link_text, 'Create an Identity'
|
24
|
+
option :registration_failure_message, 'One or more fields were invalid'
|
25
|
+
option :validation_failure_message, 'Validation failed'
|
26
|
+
option :title, 'Identity Verification' # Title for Login Form
|
27
|
+
option :registration_form_title, 'Register Identity' # Title for Registration Form
|
23
28
|
|
24
29
|
def request_phase
|
25
30
|
if options[:on_login]
|
@@ -65,29 +70,25 @@ module OmniAuth
|
|
65
70
|
end
|
66
71
|
|
67
72
|
def registration_phase
|
68
|
-
attributes = (options[:fields] +
|
73
|
+
attributes = (options[:fields] + DEFAULT_REGISTRATION_FIELDS).each_with_object({}) do |k, h|
|
69
74
|
h[k] = request[k.to_s]
|
70
75
|
end
|
71
76
|
if model.respond_to?(:column_names) && model.column_names.include?('provider')
|
72
77
|
attributes.reverse_merge!(provider: 'identity')
|
73
78
|
end
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
env['omniauth.identity'] = @identity
|
81
|
-
options[:on_failed_registration].call(env)
|
79
|
+
if validating?
|
80
|
+
@identity = model.new(attributes)
|
81
|
+
env['omniauth.identity'] = @identity
|
82
|
+
if valid?
|
83
|
+
@identity.save
|
84
|
+
registration_result
|
82
85
|
else
|
83
|
-
|
84
|
-
registration_form(validation_message)
|
86
|
+
registration_failure(options[:validation_failure_message])
|
85
87
|
end
|
86
|
-
elsif @identity.save && @identity.persisted?
|
87
|
-
env['PATH_INFO'] = callback_path
|
88
|
-
callback_phase
|
89
88
|
else
|
90
|
-
|
89
|
+
@identity = model.create(attributes)
|
90
|
+
env['omniauth.identity'] = @identity
|
91
|
+
registration_result
|
91
92
|
end
|
92
93
|
end
|
93
94
|
|
@@ -120,19 +121,19 @@ module OmniAuth
|
|
120
121
|
|
121
122
|
def build_omniauth_login_form
|
122
123
|
OmniAuth::Form.build(
|
123
|
-
title:
|
124
|
+
title: options[:title],
|
124
125
|
url: callback_path
|
125
126
|
) do |f|
|
126
127
|
f.text_field 'Login', 'auth_key'
|
127
128
|
f.password_field 'Password', 'password'
|
128
129
|
if options[:enable_registration]
|
129
|
-
f.html "<p align='center'><a href='#{registration_path}'
|
130
|
+
f.html "<p align='center'><a href='#{registration_path}'>#{options[:create_identity_link_text]}</a></p>"
|
130
131
|
end
|
131
132
|
end
|
132
133
|
end
|
133
134
|
|
134
135
|
def build_omniauth_registration_form(validation_message)
|
135
|
-
OmniAuth::Form.build(title:
|
136
|
+
OmniAuth::Form.build(title: options[:registration_form_title]) do |f|
|
136
137
|
f.html "<p style='color:red'>#{validation_message}</p>" if validation_message
|
137
138
|
options[:fields].each do |field|
|
138
139
|
f.text_field field.to_s.capitalize, field.to_s
|
@@ -142,13 +143,36 @@ module OmniAuth
|
|
142
143
|
end
|
143
144
|
end
|
144
145
|
|
145
|
-
|
146
|
+
# Validates the model before it is persisted
|
147
|
+
#
|
148
|
+
# @return [truthy or falsey] :on_validation option is truthy or falsey
|
149
|
+
def validating?
|
150
|
+
!!options[:on_validation]
|
151
|
+
end
|
152
|
+
|
153
|
+
# Validates the model before it is persisted
|
154
|
+
#
|
155
|
+
# @return [true or false] result of :on_validation call
|
156
|
+
def valid?
|
157
|
+
# on_validation may run a Captcha or other validation mechanism
|
158
|
+
# Must return true when validation passes, false otherwise
|
159
|
+
!!options[:on_validation].call(env: env)
|
160
|
+
end
|
161
|
+
|
162
|
+
def registration_failure(message)
|
146
163
|
if options[:on_failed_registration]
|
147
|
-
env['omniauth.identity'] = @identity
|
148
164
|
options[:on_failed_registration].call(env)
|
149
165
|
else
|
150
|
-
|
151
|
-
|
166
|
+
registration_form(message)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def registration_result
|
171
|
+
if @identity.persisted?
|
172
|
+
env['PATH_INFO'] = callback_path
|
173
|
+
callback_phase
|
174
|
+
else
|
175
|
+
registration_failure(options[:registration_failure_message])
|
152
176
|
end
|
153
177
|
end
|
154
178
|
end
|
@@ -1,123 +1,43 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class ExampleModel
|
4
|
-
include OmniAuth::Identity::Model
|
5
|
-
end
|
6
|
-
|
7
3
|
RSpec.describe OmniAuth::Identity::Model do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
describe '.locate' do
|
12
|
-
it('is abstract') { expect { subject.locate('abc') }.to raise_error(NotImplementedError) }
|
4
|
+
before do
|
5
|
+
identity_test_klass = Class.new do
|
6
|
+
include OmniAuth::Identity::Model
|
13
7
|
end
|
8
|
+
stub_const('IdentityTestClass', identity_test_klass)
|
9
|
+
end
|
14
10
|
|
15
|
-
|
16
|
-
|
17
|
-
mocked_instance = double('ExampleModel', authenticate: 'abbadoo')
|
18
|
-
allow(subject).to receive(:locate).with('email' => 'example').and_return(mocked_instance)
|
19
|
-
expect(subject.authenticate({ 'email' => 'example' }, 'pass')).to eq('abbadoo')
|
20
|
-
end
|
11
|
+
describe 'Class Methods' do
|
12
|
+
subject(:model_klass) { IdentityTestClass }
|
21
13
|
|
22
|
-
|
23
|
-
mocked_instance = double('ExampleModel', authenticate: 'abbadoo')
|
24
|
-
allow(subject).to receive(:locate).with('email' => 'example',
|
25
|
-
'user_type' => 'admin').and_return(mocked_instance)
|
26
|
-
expect(subject.authenticate({ 'email' => 'example', 'user_type' => 'admin' }, 'pass')).to eq('abbadoo')
|
27
|
-
end
|
14
|
+
include_context 'model with class methods'
|
28
15
|
|
29
|
-
|
30
|
-
|
31
|
-
expect
|
16
|
+
describe '::locate' do
|
17
|
+
it('is abstract') do
|
18
|
+
expect { model_klass.locate('email' => 'example') }.to raise_error(NotImplementedError)
|
32
19
|
end
|
33
20
|
end
|
34
21
|
end
|
35
22
|
|
36
|
-
|
37
|
-
subject {
|
23
|
+
describe 'Instance Methods' do
|
24
|
+
subject(:instance) { IdentityTestClass.new }
|
38
25
|
|
39
|
-
|
40
|
-
it('is abstract') { expect { subject.authenticate('abc') }.to raise_error(NotImplementedError) }
|
41
|
-
end
|
26
|
+
include_context 'instance with instance methods'
|
42
27
|
|
43
|
-
describe '#
|
44
|
-
it '
|
45
|
-
allow(subject).to receive(:respond_to?).with(:id).and_return(true)
|
46
|
-
allow(subject).to receive(:id).and_return 'wakka-do'
|
47
|
-
expect(subject.uid).to eq('wakka-do')
|
48
|
-
end
|
49
|
-
|
50
|
-
it 'stringifies it' do
|
51
|
-
allow(subject).to receive(:id).and_return 123
|
52
|
-
expect(subject.uid).to eq('123')
|
53
|
-
end
|
54
|
-
|
55
|
-
it 'raises NotImplementedError if #id is not defined' do
|
56
|
-
allow(subject).to receive(:respond_to?).with(:id).and_return(false)
|
57
|
-
expect { subject.uid }.to raise_error(NotImplementedError)
|
58
|
-
end
|
28
|
+
describe '#authenticate' do
|
29
|
+
it('is abstract') { expect { instance.authenticate('my-password') }.to raise_error(NotImplementedError) }
|
59
30
|
end
|
60
31
|
|
61
32
|
describe '#auth_key' do
|
62
|
-
it 'defaults to #email' do
|
63
|
-
allow(subject).to receive(:respond_to?).with(:email).and_return(true)
|
64
|
-
allow(subject).to receive(:email).and_return('bob@bob.com')
|
65
|
-
expect(subject.auth_key).to eq('bob@bob.com')
|
66
|
-
end
|
67
|
-
|
68
|
-
it 'uses the class .auth_key' do
|
69
|
-
subject.class.auth_key 'login'
|
70
|
-
allow(subject).to receive(:login).and_return 'bob'
|
71
|
-
expect(subject.auth_key).to eq('bob')
|
72
|
-
subject.class.auth_key nil
|
73
|
-
end
|
74
|
-
|
75
33
|
it 'raises a NotImplementedError if the auth_key method is not defined' do
|
76
|
-
expect {
|
34
|
+
expect { instance.auth_key }.to raise_error(NotImplementedError)
|
77
35
|
end
|
78
36
|
end
|
79
37
|
|
80
38
|
describe '#auth_key=' do
|
81
|
-
it '
|
82
|
-
|
83
|
-
expect(subject).to receive(:email=).with 'abc'
|
84
|
-
|
85
|
-
subject.auth_key = 'abc'
|
86
|
-
end
|
87
|
-
|
88
|
-
it 'uses a custom .auth_key if one is provided' do
|
89
|
-
subject.class.auth_key 'login'
|
90
|
-
allow(subject).to receive(:respond_to?).with(:login=).and_return(true)
|
91
|
-
expect(subject).to receive(:login=).with('abc')
|
92
|
-
|
93
|
-
subject.auth_key = 'abc'
|
94
|
-
end
|
95
|
-
|
96
|
-
it 'raises a NotImplementedError if the autH_key method is not defined' do
|
97
|
-
expect { subject.auth_key = 'broken' }.to raise_error(NotImplementedError)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
describe '#info' do
|
102
|
-
it 'includes attributes that are set' do
|
103
|
-
allow(subject).to receive(:name).and_return('Bob Bobson')
|
104
|
-
allow(subject).to receive(:nickname).and_return('bob')
|
105
|
-
|
106
|
-
expect(subject.info).to eq({
|
107
|
-
'name' => 'Bob Bobson',
|
108
|
-
'nickname' => 'bob'
|
109
|
-
})
|
110
|
-
end
|
111
|
-
|
112
|
-
it 'automaticallies set name off of nickname' do
|
113
|
-
allow(subject).to receive(:nickname).and_return('bob')
|
114
|
-
subject.info['name'] == 'bob'
|
115
|
-
end
|
116
|
-
|
117
|
-
it 'does not overwrite a provided name' do
|
118
|
-
allow(subject).to receive(:name).and_return('Awesome Dude')
|
119
|
-
allow(subject).to receive(:first_name).and_return('Frank')
|
120
|
-
expect(subject.info['name']).to eq('Awesome Dude')
|
39
|
+
it 'raises a NotImplementedError if the auth_key method is not defined' do
|
40
|
+
expect { instance.auth_key = 'broken' }.to raise_error(NotImplementedError)
|
121
41
|
end
|
122
42
|
end
|
123
43
|
end
|