lapis-yggdrasil 0.5.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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +18 -0
- data/.gitignore +151 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +4 -0
- data/Gemfile +8 -0
- data/LICENSE.md +16 -0
- data/README.md +130 -0
- data/Rakefile +28 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lapis-yggdrasil.gemspec +35 -0
- data/lib/lapis/yggdrasil.rb +18 -0
- data/lib/lapis/yggdrasil/agent.rb +52 -0
- data/lib/lapis/yggdrasil/authentication_client.rb +106 -0
- data/lib/lapis/yggdrasil/authentication_error.rb +29 -0
- data/lib/lapis/yggdrasil/client.rb +38 -0
- data/lib/lapis/yggdrasil/error_codes.rb +43 -0
- data/lib/lapis/yggdrasil/messaging.rb +23 -0
- data/lib/lapis/yggdrasil/messaging/authentication_request.rb +50 -0
- data/lib/lapis/yggdrasil/messaging/authentication_response.rb +36 -0
- data/lib/lapis/yggdrasil/messaging/error_response.rb +74 -0
- data/lib/lapis/yggdrasil/messaging/invalidate_request.rb +34 -0
- data/lib/lapis/yggdrasil/messaging/refresh_request.rb +47 -0
- data/lib/lapis/yggdrasil/messaging/refresh_response.rb +29 -0
- data/lib/lapis/yggdrasil/messaging/request.rb +25 -0
- data/lib/lapis/yggdrasil/messaging/response.rb +33 -0
- data/lib/lapis/yggdrasil/messaging/response_factory.rb +82 -0
- data/lib/lapis/yggdrasil/messaging/signout_request.rb +38 -0
- data/lib/lapis/yggdrasil/messaging/token_request.rb +42 -0
- data/lib/lapis/yggdrasil/messaging/token_response.rb +36 -0
- data/lib/lapis/yggdrasil/messaging/validate_request.rb +34 -0
- data/lib/lapis/yggdrasil/profile.rb +65 -0
- data/lib/lapis/yggdrasil/session.rb +60 -0
- data/lib/lapis/yggdrasil/session_info.rb +62 -0
- data/lib/lapis/yggdrasil/version.rb +5 -0
- data/spec/factories/agent_factory.rb +10 -0
- data/spec/factories/authentication_error_factory.rb +14 -0
- data/spec/factories/message_factory.rb +111 -0
- data/spec/factories/profile_factory.rb +20 -0
- data/spec/factories/session_info_factory.rb +11 -0
- data/spec/factories/uuid_factory.rb +28 -0
- data/spec/lapis/yggdrasil/agent_spec.rb +103 -0
- data/spec/lapis/yggdrasil/authentication_client_spec.rb +200 -0
- data/spec/lapis/yggdrasil/authentication_error_spec.rb +42 -0
- data/spec/lapis/yggdrasil/messaging/authentication_request_spec.rb +61 -0
- data/spec/lapis/yggdrasil/messaging/authentication_response_spec.rb +63 -0
- data/spec/lapis/yggdrasil/messaging/error_response_spec.rb +164 -0
- data/spec/lapis/yggdrasil/messaging/invalidate_request_spec.rb +29 -0
- data/spec/lapis/yggdrasil/messaging/refresh_request_spec.rb +70 -0
- data/spec/lapis/yggdrasil/messaging/refresh_response_spec.rb +50 -0
- data/spec/lapis/yggdrasil/messaging/response_factory_spec.rb +130 -0
- data/spec/lapis/yggdrasil/messaging/response_spec.rb +36 -0
- data/spec/lapis/yggdrasil/messaging/signout_request_spec.rb +29 -0
- data/spec/lapis/yggdrasil/messaging/validate_request_spec.rb +29 -0
- data/spec/lapis/yggdrasil/profile_spec.rb +108 -0
- data/spec/lapis/yggdrasil/session_info_spec.rb +131 -0
- data/spec/lapis/yggdrasil/session_spec.rb +158 -0
- data/spec/spec_helper.rb +89 -0
- metadata +269 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lapis::Yggdrasil::AuthenticationError do
|
4
|
+
|
5
|
+
describe '#code' do
|
6
|
+
let(:code) { Lapis::Yggdrasil::ErrorCodes::NOT_FOUND }
|
7
|
+
subject(:error) { build(:error, :code => code) }
|
8
|
+
subject { error.code }
|
9
|
+
|
10
|
+
it 'is the expected value' do
|
11
|
+
is_expected.to eq code
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#message' do
|
16
|
+
let(:code) { Lapis::Yggdrasil::ErrorCodes::INVALID_CREDENTIALS }
|
17
|
+
let(:error) { build(:error, :code => code) }
|
18
|
+
subject { error.to_s }
|
19
|
+
|
20
|
+
it 'contains the code' do
|
21
|
+
is_expected.to match(/#{code}/i)
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with a message' do
|
25
|
+
let(:message) { 'foobarbaz' }
|
26
|
+
let(:error) { build(:error, :message => message) }
|
27
|
+
|
28
|
+
it 'contains the message' do
|
29
|
+
is_expected.to include message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'without a message' do
|
34
|
+
let(:error) { build(:error, :no_message) }
|
35
|
+
|
36
|
+
it 'contains the class name' do
|
37
|
+
is_expected.to include Lapis::Yggdrasil::AuthenticationError.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lapis::Yggdrasil::Messaging::AuthenticationRequest do
|
4
|
+
let(:username) { 'Notch' }
|
5
|
+
let(:password) { 'qwerty' }
|
6
|
+
let(:agent) { build(:agent) }
|
7
|
+
let(:token) { build(:uuid) }
|
8
|
+
subject(:request) { Lapis::Yggdrasil::Messaging::AuthenticationRequest.new(username, password, agent, token) }
|
9
|
+
|
10
|
+
describe '#endpoint' do
|
11
|
+
subject { request.endpoint }
|
12
|
+
|
13
|
+
it 'is /authenticate' do
|
14
|
+
is_expected.to eq '/authenticate'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#to_json' do
|
19
|
+
subject(:json_str) { request.to_json }
|
20
|
+
subject(:structure) { JSON.parse(json_str) }
|
21
|
+
|
22
|
+
it 'contains the username' do
|
23
|
+
is_expected.to include('username' => username)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'contains the password' do
|
27
|
+
is_expected.to include('password' => password)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'contains the agent info' do
|
31
|
+
is_expected.to include('agent')
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'the agent' do
|
35
|
+
subject { structure['agent'] }
|
36
|
+
|
37
|
+
it 'contains the name' do
|
38
|
+
is_expected.to include('name' => agent.name)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'contains the version' do
|
42
|
+
is_expected.to include('version' => agent.version)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'with a client token' do
|
47
|
+
it 'contains the client token' do
|
48
|
+
is_expected.to include('clientToken' => token.to_s(false))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'without a client token' do
|
53
|
+
let(:token) { nil }
|
54
|
+
|
55
|
+
it 'does not contain a token entry' do
|
56
|
+
is_expected.not_to include('clientToken')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lapis::Yggdrasil::Messaging::AuthenticationResponse do
|
4
|
+
let(:access) { build(:uuid) }
|
5
|
+
let(:client) { build(:uuid) }
|
6
|
+
let(:profiles) { build_list(:profile, 5) }
|
7
|
+
let(:selected) { profiles.first }
|
8
|
+
let(:message) { build(:auth_message, :access => access, :client => client, :profiles => profiles, :selected => selected) }
|
9
|
+
subject(:response) { Lapis::Yggdrasil::Messaging::AuthenticationResponse.new(message) }
|
10
|
+
|
11
|
+
describe '#client_token' do
|
12
|
+
subject { response.client_token }
|
13
|
+
|
14
|
+
it 'is the expected value' do
|
15
|
+
is_expected.to eq client
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#access_token' do
|
20
|
+
subject { response.access_token }
|
21
|
+
|
22
|
+
it 'is the expected value' do
|
23
|
+
is_expected.to eq access
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#available_profiles' do
|
28
|
+
subject { response.available_profiles }
|
29
|
+
|
30
|
+
it 'has the expected profiles' do
|
31
|
+
is_expected.to contain_exactly(*profiles)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'is frozen' do
|
35
|
+
is_expected.to be_frozen
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#selected_profile' do
|
40
|
+
subject { response.selected_profile }
|
41
|
+
|
42
|
+
it 'is the expected value' do
|
43
|
+
is_expected.to eq selected
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#status' do
|
48
|
+
subject { response.status }
|
49
|
+
|
50
|
+
it 'is the expected value' do
|
51
|
+
is_expected.to eq message.status
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#ok?' do
|
56
|
+
subject { response.ok? }
|
57
|
+
|
58
|
+
it 'is the expected value' do
|
59
|
+
is_expected.to eq message.ok?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lapis::Yggdrasil::Messaging::ErrorResponse do
|
4
|
+
let(:error) { 'Not Found' }
|
5
|
+
let(:error_message) { 'The server has not found anything matching the request URI' }
|
6
|
+
let(:cause) { 'Testing' }
|
7
|
+
let(:message) { build(:error_message, :error => error, :error_message => error_message, :cause => cause) }
|
8
|
+
subject(:response) { Lapis::Yggdrasil::Messaging::ErrorResponse.new(message) }
|
9
|
+
|
10
|
+
describe '#error' do
|
11
|
+
subject { response.error }
|
12
|
+
|
13
|
+
it 'is the expected value' do
|
14
|
+
is_expected.to eq error
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#message' do
|
19
|
+
subject { response.message }
|
20
|
+
|
21
|
+
it 'is the expected value' do
|
22
|
+
is_expected.to eq error_message
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#cause' do
|
27
|
+
subject { response.cause }
|
28
|
+
|
29
|
+
it 'is the expected value' do
|
30
|
+
is_expected.to eq cause
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'without a cause' do
|
34
|
+
let(:message) { build(:error_message, :no_cause) }
|
35
|
+
|
36
|
+
it 'is nil' do
|
37
|
+
is_expected.to eq nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#status' do
|
43
|
+
subject { response.status }
|
44
|
+
|
45
|
+
it 'is the expected value' do
|
46
|
+
is_expected.to eq message.status
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#ok?' do
|
51
|
+
subject { response.ok? }
|
52
|
+
|
53
|
+
it 'is the expected value' do
|
54
|
+
is_expected.to eq message.ok?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#to_error' do
|
59
|
+
subject(:ex) { response.to_error }
|
60
|
+
|
61
|
+
describe '.code' do
|
62
|
+
subject { ex.code }
|
63
|
+
|
64
|
+
context 'with method not allowed' do
|
65
|
+
let(:error) { 'Method Not Allowed' }
|
66
|
+
let(:error_message) { 'The method specified in the request is not allowed for the resource identified by the request URI' }
|
67
|
+
let(:cause) { nil }
|
68
|
+
|
69
|
+
it 'is METHOD_NOT_ALLOWED' do
|
70
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::METHOD_NOT_ALLOWED
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'with resource not found' do
|
75
|
+
let(:error) { 'Not Found' }
|
76
|
+
let(:error_message) { 'The server has not found anything matching the request URI' }
|
77
|
+
let(:cause) { nil }
|
78
|
+
|
79
|
+
it 'is NOT_FOUND' do
|
80
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::NOT_FOUND
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with a migrated account' do
|
85
|
+
let(:error) { 'ForbiddenOperationException' }
|
86
|
+
let(:error_message) { 'Invalid credentials. Account migrated, use e-mail as username.' }
|
87
|
+
let(:cause) { 'UserMigratedException' }
|
88
|
+
|
89
|
+
it 'is ACCOUNT_MIGRATED' do
|
90
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::ACCOUNT_MIGRATED
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'with wrong credentials' do
|
95
|
+
let(:error) { 'ForbiddenOperationException' }
|
96
|
+
let(:error_message) { 'Invalid credentials. Invalid username or password.' }
|
97
|
+
let(:cause) { nil }
|
98
|
+
|
99
|
+
it 'is INVALID_CREDENTIALS' do
|
100
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::INVALID_CREDENTIALS
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'with too many attempts' do
|
105
|
+
let(:error) { 'ForbiddenOperationException' }
|
106
|
+
let(:error_message) { 'Invalid credentials.' }
|
107
|
+
let(:cause) { nil }
|
108
|
+
|
109
|
+
it 'is TOO_MANY_ATTEMPTS' do
|
110
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::TOO_MANY_ATTEMPTS
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'with an invalid token' do
|
115
|
+
let(:error) { 'ForbiddenOperationException' }
|
116
|
+
let(:error_message) { 'Invalid token.' }
|
117
|
+
let(:cause) { nil }
|
118
|
+
|
119
|
+
it 'is INVALID_TOKEN' do
|
120
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::INVALID_TOKEN
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'with a profile' do
|
125
|
+
let(:error) { 'IllegalArgumentException' }
|
126
|
+
let(:error_message) { 'Access token already has a profile assigned.' }
|
127
|
+
let(:cause) { nil }
|
128
|
+
|
129
|
+
it 'is PROFILE_NOT_SUPPORTED' do
|
130
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::PROFILE_NOT_SUPPORTED
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'with no credentials' do
|
135
|
+
let(:error) { 'IllegalArgumentException' }
|
136
|
+
let(:error_message) { 'credentials is null' }
|
137
|
+
let(:cause) { nil }
|
138
|
+
|
139
|
+
it 'is NO_CREDENTIALS' do
|
140
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::NO_CREDENTIALS
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'with wrong content-type' do
|
145
|
+
let(:error) { 'Unsupported Media Type' }
|
146
|
+
let(:error_message) { 'The server is refusing to service the request because the entity of the request is in a format not supported by the requested resource for the requested method' }
|
147
|
+
let(:cause) { nil }
|
148
|
+
|
149
|
+
it 'is UNSUPPORTED_MEDIA_TYPE' do
|
150
|
+
is_expected.to eq Lapis::Yggdrasil::ErrorCodes::UNSUPPORTED_MEDIA_TYPE
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe '.message' do
|
156
|
+
subject { ex.message }
|
157
|
+
|
158
|
+
it 'is passed along' do
|
159
|
+
is_expected.to include error_message
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lapis::Yggdrasil::Messaging::InvalidateRequest do
|
4
|
+
let(:client) { build(:uuid) }
|
5
|
+
let(:access) { build(:uuid) }
|
6
|
+
subject(:request) { Lapis::Yggdrasil::Messaging::InvalidateRequest.new(client, access) }
|
7
|
+
|
8
|
+
describe '#endpoint' do
|
9
|
+
subject { request.endpoint }
|
10
|
+
|
11
|
+
it 'is /invalidate' do
|
12
|
+
is_expected.to eq '/invalidate'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#to_json' do
|
17
|
+
subject(:json_str) { request.to_json }
|
18
|
+
subject(:structure) { JSON.parse(json_str) }
|
19
|
+
|
20
|
+
it 'contains the client token' do
|
21
|
+
is_expected.to include('clientToken' => client.to_s(false))
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'contains the access token' do
|
25
|
+
is_expected.to include('accessToken' => access.to_s(false))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lapis::Yggdrasil::Messaging::RefreshRequest do
|
4
|
+
let(:access) { build(:uuid) }
|
5
|
+
let(:client) { build(:uuid) }
|
6
|
+
let(:profile) { build(:profile) }
|
7
|
+
subject(:request) { Lapis::Yggdrasil::Messaging::RefreshRequest.new(client, access, profile) }
|
8
|
+
|
9
|
+
describe '#endpoint' do
|
10
|
+
subject { request.endpoint }
|
11
|
+
|
12
|
+
it 'is /refresh' do
|
13
|
+
is_expected.to eq '/refresh'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#to_json' do
|
18
|
+
subject(:json_str) { request.to_json }
|
19
|
+
subject(:structure) { JSON.parse(json_str) }
|
20
|
+
|
21
|
+
it 'contains the access token' do
|
22
|
+
is_expected.to include('accessToken' => access.to_s(false))
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'contains the client token' do
|
26
|
+
is_expected.to include('clientToken' => client.to_s(false))
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'contains the profile info' do
|
30
|
+
is_expected.to include('selectedProfile')
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'the profile' do
|
34
|
+
subject { structure['selectedProfile'] }
|
35
|
+
|
36
|
+
it 'contains the name' do
|
37
|
+
is_expected.to include('name' => profile.name)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'contains the ID' do
|
41
|
+
is_expected.to include('id' => profile.id.to_s(false))
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with a legacy profile' do
|
45
|
+
let(:profile) { build(:profile, :legacy) }
|
46
|
+
|
47
|
+
it 'contains the legacy flag' do
|
48
|
+
is_expected.to include('legacy' => true)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'with a migrated profile' do
|
53
|
+
let(:profile) { build(:profile, :modern) }
|
54
|
+
|
55
|
+
it 'does not contain the legacy flag' do
|
56
|
+
is_expected.not_to include('legacy')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'without a profile' do
|
62
|
+
let(:profile) { nil }
|
63
|
+
|
64
|
+
it 'does not contain a profile entry' do
|
65
|
+
is_expected.not_to include('selectedProfile')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lapis::Yggdrasil::Messaging::RefreshResponse do
|
4
|
+
let(:access) { build(:uuid) }
|
5
|
+
let(:client) { build(:uuid) }
|
6
|
+
let(:profile) { build(:profile) }
|
7
|
+
let(:message) { build(:refresh_message, :access => access, :client => client, :profile => profile) }
|
8
|
+
subject(:response) { Lapis::Yggdrasil::Messaging::RefreshResponse.new(message) }
|
9
|
+
|
10
|
+
describe '#client_token' do
|
11
|
+
subject { response.client_token }
|
12
|
+
|
13
|
+
it 'is the expected value' do
|
14
|
+
is_expected.to eq client
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#access_token' do
|
19
|
+
subject { response.access_token }
|
20
|
+
|
21
|
+
it 'is the expected value' do
|
22
|
+
is_expected.to eq access
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#profile' do
|
27
|
+
subject { response.profile }
|
28
|
+
|
29
|
+
it 'is the expected value' do
|
30
|
+
is_expected.to eq profile
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#status' do
|
35
|
+
subject { response.status }
|
36
|
+
|
37
|
+
it 'is the expected value' do
|
38
|
+
is_expected.to eq message.status
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#ok?' do
|
43
|
+
subject { response.ok? }
|
44
|
+
|
45
|
+
it 'is the expected value' do
|
46
|
+
is_expected.to eq message.ok?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|