signinable 2.0.10 → 2.0.13
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.
- checksums.yaml +4 -4
- data/Rakefile +6 -4
- data/app/models/signin.rb +24 -5
- data/config/routes.rb +2 -0
- data/db/migrate/20140103165607_create_signins.rb +7 -5
- data/db/migrate/20180530131006_add_custom_data_to_sigins.rb +5 -5
- data/lib/signinable/engine.rb +6 -9
- data/lib/signinable/model_additions.rb +117 -51
- data/lib/signinable/version.rb +3 -1
- data/lib/signinable.rb +3 -1
- data/spec/dummy/Rakefile +3 -1
- data/spec/dummy/app/models/user.rb +3 -1
- data/spec/dummy/bin/bundle +3 -1
- data/spec/dummy/bin/rails +3 -1
- data/spec/dummy/bin/rake +2 -0
- data/spec/dummy/config/application.rb +5 -20
- data/spec/dummy/config/boot.rb +5 -3
- data/spec/dummy/config/environment.rb +3 -1
- data/spec/dummy/config/environments/development.rb +4 -2
- data/spec/dummy/config/environments/production.rb +2 -0
- data/spec/dummy/config/environments/test.rb +4 -2
- data/spec/dummy/config/initializers/backtrace_silencers.rb +1 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +2 -0
- data/spec/dummy/config/initializers/inflections.rb +1 -0
- data/spec/dummy/config/initializers/mime_types.rb +1 -0
- data/spec/dummy/config/initializers/secret_token.rb +2 -0
- data/spec/dummy/config/initializers/session_store.rb +2 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +2 -0
- data/spec/dummy/config/routes.rb +2 -0
- data/spec/dummy/config.ru +3 -1
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20140103165606_create_users.rb +3 -1
- data/spec/dummy/db/schema.rb +23 -24
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +427 -0
- data/spec/dummy/log/test.log +20023 -0
- data/spec/factories/signins.rb +8 -0
- data/spec/factories/users.rb +7 -0
- data/spec/models/signin_spec.rb +85 -33
- data/spec/models/user_spec.rb +204 -84
- data/spec/rails_helper.rb +20 -0
- data/spec/spec_helper.rb +11 -12
- data/spec/support/utilities.rb +3 -2
- metadata +76 -38
- data/spec/factories/signin.rb +0 -8
- data/spec/factories/user.rb +0 -7
data/spec/models/signin_spec.rb
CHANGED
@@ -1,50 +1,102 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
2
4
|
|
3
5
|
describe Signin do
|
4
|
-
|
5
|
-
|
6
|
-
signin
|
7
|
-
end
|
6
|
+
describe '#expired?' do
|
7
|
+
let(:expiration_time) { Time.zone.now + 1.hour }
|
8
|
+
let(:signin) { create(:signin, expiration_time: expiration_time) }
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
signin = FactoryGirl.create(:signin)
|
12
|
-
signin.token = nil
|
13
|
-
signin.should_not be_valid
|
10
|
+
it 'returns false when not expired' do
|
11
|
+
expect(signin).to_not be_expired
|
14
12
|
end
|
15
13
|
|
16
|
-
it
|
17
|
-
|
14
|
+
it 'returns true when expired' do
|
15
|
+
Timecop.travel(expiration_time) do
|
16
|
+
expect(signin).to be_expired
|
17
|
+
end
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
describe '#expireable?' do
|
22
|
+
it 'returns false when expireable' do
|
23
|
+
signin = create(:signin, expiration_time: nil)
|
24
|
+
expect(signin).to_not be_expireable
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
it 'returns true when expireable' do
|
28
|
+
signin = create(:signin, expiration_time: Time.zone.now)
|
29
|
+
expect(signin).to be_expireable
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
describe '#expire!' do
|
34
|
+
it 'sets expiration_time to now' do
|
35
|
+
signin = create(:signin, expiration_time: (Time.zone.now + 1.hour))
|
36
|
+
allow(signin).to receive(:renew!)
|
37
|
+
signin.expire!
|
38
|
+
expect(signin).to have_received(:renew!).with(period: 0, ip: signin.ip, user_agent: signin.user_agent)
|
39
|
+
end
|
39
40
|
end
|
40
41
|
|
41
|
-
describe
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
describe '#renew!' do
|
43
|
+
let(:signin) { create(:signin) }
|
44
|
+
let(:attrs) do
|
45
|
+
{
|
46
|
+
period: 100,
|
47
|
+
ip: signin.ip,
|
48
|
+
user_agent: signin.user_agent,
|
49
|
+
refresh_token: false
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
before(:each) do
|
54
|
+
allow(signin).to receive(:update!)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'updates ip and user_agent' do
|
58
|
+
signin.renew!(**attrs)
|
59
|
+
expect(signin).to have_received(:update!).with(hash_including(ip: signin.ip, user_agent: signin.user_agent))
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when expireable' do
|
63
|
+
before(:each) do
|
64
|
+
allow(signin).to receive(:expireable?).and_return(true)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'updates expiration_time' do
|
68
|
+
Timecop.freeze do
|
69
|
+
signin.renew!(**attrs)
|
70
|
+
expect(signin).to have_received(:update!).with(hash_including(expiration_time: Time.zone.now + attrs[:period]))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when not expireable' do
|
76
|
+
before(:each) do
|
77
|
+
allow(signin).to receive(:expireable?).and_return(false)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'does not update expiration_time' do
|
81
|
+
signin.renew!(**attrs)
|
82
|
+
expect(signin).to have_received(:update!).with(hash_excluding(expiration_time: Time.zone.now + attrs[:period]))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when need to refresh_token' do
|
87
|
+
it 'updates expiration_time' do
|
88
|
+
allow(SecureRandom).to receive(:urlsafe_base64).and_return('bla')
|
89
|
+
signin.renew!(**attrs.merge(refresh_token: true))
|
90
|
+
expect(signin).to have_received(:update!).with(hash_including(token: 'bla'))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when no need to refresh_token' do
|
95
|
+
it 'does not update expiration_time' do
|
96
|
+
allow(SecureRandom).to receive(:urlsafe_base64).and_return('bla')
|
97
|
+
signin.renew!(**attrs)
|
98
|
+
expect(signin).to have_received(:update!).with(hash_excluding(token: 'bla'))
|
99
|
+
end
|
48
100
|
end
|
49
101
|
end
|
50
102
|
end
|
data/spec/models/user_spec.rb
CHANGED
@@ -1,133 +1,253 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
2
4
|
|
3
5
|
describe User do
|
6
|
+
let(:credentials) { ['127.0.0.1', 'user_agent'] }
|
7
|
+
let(:other_credentials) { ['127.0.0.2', 'user_agent2'] }
|
8
|
+
let(:user) { create(:user) }
|
9
|
+
|
4
10
|
before :each do
|
5
11
|
Timecop.freeze
|
6
|
-
User.signin_expiration = 2.hours
|
7
|
-
User.signin_simultaneous = true
|
8
|
-
User.signin_restrictions = []
|
9
|
-
@user = FactoryGirl.create(:user)
|
10
|
-
@credentials = ['127.0.0.1', 'user_agent']
|
11
|
-
@other_credentials = ['127.0.0.2', 'user_agent2']
|
12
12
|
end
|
13
13
|
|
14
14
|
after :each do
|
15
15
|
Timecop.return
|
16
16
|
end
|
17
17
|
|
18
|
-
describe
|
19
|
-
it
|
20
|
-
expect
|
21
|
-
sign_in_user(
|
22
|
-
|
18
|
+
describe '#signin' do
|
19
|
+
it 'should create Signin' do
|
20
|
+
expect do
|
21
|
+
sign_in_user(user, credentials)
|
22
|
+
end.to change(Signin, :count).by(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'sets jwt on user instance' do
|
26
|
+
expect do
|
27
|
+
sign_in_user(user, credentials)
|
28
|
+
end.to change(user, :jwt).from(nil)
|
23
29
|
end
|
24
30
|
|
25
|
-
it
|
26
|
-
|
27
|
-
signin
|
31
|
+
it 'should generate jwt with correct payload' do
|
32
|
+
sign_in_user(user, credentials)
|
33
|
+
signin = user.last_signin
|
34
|
+
payload = JWT.decode(user.jwt, 'test', true, { algorithm: 'HS256' })[0]
|
35
|
+
expect(payload).to include(
|
36
|
+
'refresh_token' => signin.token,
|
37
|
+
'signinable_id' => user.id
|
38
|
+
)
|
28
39
|
end
|
29
40
|
|
30
|
-
it
|
31
|
-
|
32
|
-
signin =
|
33
|
-
signin.expiration_time.
|
41
|
+
it 'should set expiration_time' do
|
42
|
+
sign_in_user(user, credentials)
|
43
|
+
signin = user.last_signin
|
44
|
+
expect(signin.expiration_time.to_i).to eq((Time.zone.now + User.refresh_exp).to_i)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should not set expiration_time' do
|
48
|
+
allow(described_class).to receive(:refresh_exp).and_return(0)
|
49
|
+
sign_in_user(user, credentials)
|
50
|
+
signin = user.last_signin
|
51
|
+
expect(signin.expiration_time).to be_nil
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'when simultaneous signins enabled' do
|
55
|
+
before do
|
56
|
+
allow(described_class).to receive(:simultaneous_signings).and_return(true)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'does not expire active signins' do
|
60
|
+
sign_in_user(user, credentials)
|
61
|
+
sign_in_user(user, credentials)
|
62
|
+
expect(user.signins.active.count).to eq(2)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when simultaneous signins disabled' do
|
67
|
+
before do
|
68
|
+
allow(described_class).to receive(:simultaneous_signings).and_return(false)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'expires active signins' do
|
72
|
+
sign_in_user(user, credentials)
|
73
|
+
sign_in_user(user, credentials)
|
74
|
+
expect(user.signins.active.count).to eq(1)
|
75
|
+
end
|
34
76
|
end
|
35
77
|
end
|
36
78
|
|
37
|
-
describe
|
38
|
-
it
|
39
|
-
|
40
|
-
|
41
|
-
signin
|
42
|
-
signin.
|
79
|
+
describe '#signout' do
|
80
|
+
it 'should expire signin' do
|
81
|
+
sign_in_user(user, credentials)
|
82
|
+
signin = user.last_signin
|
83
|
+
sign_out_user(signin, credentials)
|
84
|
+
expect(signin.reload).to be_expired
|
43
85
|
end
|
44
86
|
|
45
|
-
context
|
46
|
-
%
|
47
|
-
it "
|
48
|
-
|
49
|
-
|
87
|
+
context 'when has no restrictions' do
|
88
|
+
%i[ip user_agent].each do |c|
|
89
|
+
it "allows signout when #{c} changes" do
|
90
|
+
sign_in_user(user, credentials)
|
91
|
+
signin = user.last_signin
|
92
|
+
expect(sign_out_user(signin, credentials)).to be_truthy
|
50
93
|
end
|
51
94
|
end
|
52
95
|
end
|
53
96
|
|
54
|
-
context
|
55
|
-
%
|
56
|
-
it "
|
57
|
-
|
58
|
-
|
59
|
-
|
97
|
+
context 'when has restrictions' do
|
98
|
+
%i[ip user_agent].each do |c|
|
99
|
+
it "forbids signout when #{c} changes" do
|
100
|
+
allow(described_class).to receive(:signin_restrictions).and_return([c])
|
101
|
+
sign_in_user(user, credentials)
|
102
|
+
signin = user.last_signin
|
103
|
+
expect(sign_out_user(signin, other_credentials)).to be_nil
|
60
104
|
end
|
61
105
|
end
|
62
106
|
end
|
63
107
|
end
|
64
108
|
|
65
|
-
describe
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
109
|
+
describe '#last_signin' do
|
110
|
+
it 'retuns nil when no signins' do
|
111
|
+
expect(user.last_signin).to be_nil
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'returns last active signin' do
|
115
|
+
sign_in_user(user, credentials)
|
116
|
+
sign_in_user(user, credentials)
|
117
|
+
signin = user.signins.active.last
|
118
|
+
sign_in_user(user, credentials)
|
119
|
+
user.signins.last.expire!
|
120
|
+
|
121
|
+
expect(user.last_signin).to eq(signin)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '.generate_jwt' do
|
126
|
+
let(:token) { SecureRandom.urlsafe_base64(rand(50..100)) }
|
127
|
+
|
128
|
+
it 'sets correct payload' do
|
129
|
+
jwt = described_class.generate_jwt(token, user.id)
|
130
|
+
payload = JWT.decode(jwt, described_class.jwt_secret, true, { algorithm: 'HS256' })[0]
|
131
|
+
expect(payload).to eq(
|
132
|
+
'refresh_token' => token,
|
133
|
+
'signinable_id' => user.id,
|
134
|
+
'exp' => Time.zone.now.to_i + described_class.jwt_exp
|
135
|
+
)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe '.authenticate_with_token' do
|
140
|
+
context 'when jwt is invalid' do
|
141
|
+
it 'returns nil' do
|
142
|
+
expect(described_class.authenticate_with_token('blablabla', *credentials)).to be_nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'when jwt has not expired' do
|
147
|
+
before(:each) do
|
148
|
+
sign_in_user(user, credentials)
|
75
149
|
end
|
76
150
|
|
77
|
-
it
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
151
|
+
it 'returns user' do
|
152
|
+
expect(described_class.authenticate_with_token(user.jwt, *credentials)).to eq(user)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'does not update refresh token' do
|
156
|
+
allow(described_class).to receive(:refresh_jwt)
|
157
|
+
described_class.authenticate_with_token(user.jwt, *credentials)
|
158
|
+
expect(described_class).not_to have_received(:refresh_jwt)
|
85
159
|
end
|
86
160
|
end
|
87
161
|
|
88
|
-
context
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
User.authenticate_with_token(signin1.token, *@credentials).should eq(@user)
|
93
|
-
User.authenticate_with_token(signin2.token, *@credentials).should eq(@user)
|
162
|
+
context 'when jwt has expired' do
|
163
|
+
before(:each) do
|
164
|
+
sign_in_user(user, credentials)
|
165
|
+
Timecop.travel(Time.zone.now + described_class.jwt_exp)
|
94
166
|
end
|
95
167
|
|
96
|
-
it
|
97
|
-
|
98
|
-
|
168
|
+
it 'does not do user lookup' do
|
169
|
+
allow(described_class).to receive(:find_by)
|
170
|
+
described_class.authenticate_with_token(user.jwt, *credentials)
|
171
|
+
expect(described_class).not_to have_received(:find_by)
|
99
172
|
end
|
100
173
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
end
|
174
|
+
it 'calls for refresh token' do
|
175
|
+
allow(described_class).to receive(:refresh_jwt)
|
176
|
+
described_class.authenticate_with_token(user.jwt, *credentials)
|
177
|
+
expect(described_class).to have_received(:refresh_jwt)
|
106
178
|
end
|
107
179
|
end
|
180
|
+
end
|
108
181
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
signin2 = sign_in_user(@user, @credentials)
|
114
|
-
User.authenticate_with_token(signin1.token, *@credentials).should be_nil
|
115
|
-
User.authenticate_with_token(signin2.token, *@credentials).should eq(@user)
|
182
|
+
describe '.refresh_jwt' do
|
183
|
+
context 'when jwt is invalid' do
|
184
|
+
it 'returns nil' do
|
185
|
+
expect(described_class.refresh_jwt('blablabla', *credentials)).to be_nil
|
116
186
|
end
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'returns nil when signin not found' do
|
190
|
+
jwt = described_class.generate_jwt('blablabla', 0)
|
191
|
+
expect(described_class.refresh_jwt(jwt, *credentials)).to be_nil
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'returns nil when signin expired' do
|
195
|
+
sign_in_user(user, credentials)
|
196
|
+
signin = user.last_signin
|
197
|
+
Timecop.travel(Time.zone.now + described_class.refresh_exp)
|
198
|
+
expect(described_class.refresh_jwt(user.jwt, *credentials)).to be_nil
|
199
|
+
end
|
117
200
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
201
|
+
context 'when has no restrictions' do
|
202
|
+
%i[ip user_agent].each do |c|
|
203
|
+
it "allows signin when #{c} changed" do
|
204
|
+
sign_in_user(user, credentials)
|
205
|
+
expect(described_class.refresh_jwt(user.jwt, *other_credentials)).to eq(user)
|
206
|
+
end
|
122
207
|
end
|
208
|
+
end
|
123
209
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
210
|
+
context 'when has restrictions' do
|
211
|
+
%i[ip user_agent].each do |c|
|
212
|
+
it "forbids signin when #{c} changed" do
|
213
|
+
allow(User).to receive(:signin_restrictions).and_return([c])
|
214
|
+
sign_in_user(user, credentials)
|
215
|
+
expect(described_class.refresh_jwt(user.jwt, *other_credentials)).to be_nil
|
129
216
|
end
|
130
217
|
end
|
131
218
|
end
|
219
|
+
|
220
|
+
it 'renews signin' do
|
221
|
+
sign_in_user(user, credentials)
|
222
|
+
signin = user.last_signin
|
223
|
+
allow(signin).to receive(:renew!)
|
224
|
+
allow(Signin).to receive(:find_by).with(token: signin.token).and_return(signin)
|
225
|
+
|
226
|
+
described_class.refresh_jwt(user.jwt, *credentials)
|
227
|
+
expect(signin).to have_received(:renew!).with(period: described_class.expiration_period, ip: credentials[0],
|
228
|
+
user_agent: credentials[1], refresh_token: true)
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'assigns new jwt' do
|
232
|
+
sign_in_user(user, credentials)
|
233
|
+
signin = user.last_signin
|
234
|
+
allow(user).to receive(:jwt=)
|
235
|
+
allow(signin).to receive(:signinable).and_return(user)
|
236
|
+
allow(Signin).to receive(:find_by).with(token: signin.token).and_return(signin)
|
237
|
+
allow(described_class).to receive(:generate_jwt).and_return('bla')
|
238
|
+
|
239
|
+
described_class.refresh_jwt(user.jwt, *credentials)
|
240
|
+
expect(user).to have_received(:jwt=).with('bla')
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'regenerates jwt' do
|
244
|
+
sign_in_user(user, credentials)
|
245
|
+
signin = user.last_signin
|
246
|
+
allow(described_class).to receive(:generate_jwt)
|
247
|
+
|
248
|
+
described_class.refresh_jwt(user.jwt, *credentials)
|
249
|
+
signin.reload
|
250
|
+
expect(described_class).to have_received(:generate_jwt).with(signin.token, signin.signinable_id)
|
251
|
+
end
|
132
252
|
end
|
133
253
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV['RAILS_ENV'] ||= 'test'
|
4
|
+
|
5
|
+
require File.expand_path('dummy/config/environment', __dir__)
|
6
|
+
|
7
|
+
require 'spec_helper'
|
8
|
+
require 'rspec/rails'
|
9
|
+
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.use_transactional_fixtures = true
|
14
|
+
|
15
|
+
config.infer_spec_type_from_file_location!
|
16
|
+
|
17
|
+
config.filter_rails_from_backtrace!
|
18
|
+
|
19
|
+
config.include FactoryBot::Syntax::Methods
|
20
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,18 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require '
|
5
|
-
require 'rspec/autorun'
|
6
|
-
require 'factory_girl_rails'
|
3
|
+
require 'factory_bot_rails'
|
4
|
+
require 'pry'
|
7
5
|
require 'timecop'
|
8
6
|
|
9
|
-
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.expect_with :rspec do |expectations|
|
9
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
+
config.mock_with :rspec do |mocks|
|
13
|
+
mocks.verify_partial_doubles = true
|
14
|
+
end
|
12
15
|
|
13
|
-
|
14
|
-
config.mock_with :rspec
|
15
|
-
config.use_transactional_fixtures = false
|
16
|
-
config.infer_base_class_for_anonymous_controllers = false
|
17
|
-
config.order = 'random'
|
16
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
18
17
|
end
|
data/spec/support/utilities.rb
CHANGED