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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +18 -0
  3. data/.gitignore +151 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +4 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE.md +16 -0
  9. data/README.md +130 -0
  10. data/Rakefile +28 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +7 -0
  13. data/lapis-yggdrasil.gemspec +35 -0
  14. data/lib/lapis/yggdrasil.rb +18 -0
  15. data/lib/lapis/yggdrasil/agent.rb +52 -0
  16. data/lib/lapis/yggdrasil/authentication_client.rb +106 -0
  17. data/lib/lapis/yggdrasil/authentication_error.rb +29 -0
  18. data/lib/lapis/yggdrasil/client.rb +38 -0
  19. data/lib/lapis/yggdrasil/error_codes.rb +43 -0
  20. data/lib/lapis/yggdrasil/messaging.rb +23 -0
  21. data/lib/lapis/yggdrasil/messaging/authentication_request.rb +50 -0
  22. data/lib/lapis/yggdrasil/messaging/authentication_response.rb +36 -0
  23. data/lib/lapis/yggdrasil/messaging/error_response.rb +74 -0
  24. data/lib/lapis/yggdrasil/messaging/invalidate_request.rb +34 -0
  25. data/lib/lapis/yggdrasil/messaging/refresh_request.rb +47 -0
  26. data/lib/lapis/yggdrasil/messaging/refresh_response.rb +29 -0
  27. data/lib/lapis/yggdrasil/messaging/request.rb +25 -0
  28. data/lib/lapis/yggdrasil/messaging/response.rb +33 -0
  29. data/lib/lapis/yggdrasil/messaging/response_factory.rb +82 -0
  30. data/lib/lapis/yggdrasil/messaging/signout_request.rb +38 -0
  31. data/lib/lapis/yggdrasil/messaging/token_request.rb +42 -0
  32. data/lib/lapis/yggdrasil/messaging/token_response.rb +36 -0
  33. data/lib/lapis/yggdrasil/messaging/validate_request.rb +34 -0
  34. data/lib/lapis/yggdrasil/profile.rb +65 -0
  35. data/lib/lapis/yggdrasil/session.rb +60 -0
  36. data/lib/lapis/yggdrasil/session_info.rb +62 -0
  37. data/lib/lapis/yggdrasil/version.rb +5 -0
  38. data/spec/factories/agent_factory.rb +10 -0
  39. data/spec/factories/authentication_error_factory.rb +14 -0
  40. data/spec/factories/message_factory.rb +111 -0
  41. data/spec/factories/profile_factory.rb +20 -0
  42. data/spec/factories/session_info_factory.rb +11 -0
  43. data/spec/factories/uuid_factory.rb +28 -0
  44. data/spec/lapis/yggdrasil/agent_spec.rb +103 -0
  45. data/spec/lapis/yggdrasil/authentication_client_spec.rb +200 -0
  46. data/spec/lapis/yggdrasil/authentication_error_spec.rb +42 -0
  47. data/spec/lapis/yggdrasil/messaging/authentication_request_spec.rb +61 -0
  48. data/spec/lapis/yggdrasil/messaging/authentication_response_spec.rb +63 -0
  49. data/spec/lapis/yggdrasil/messaging/error_response_spec.rb +164 -0
  50. data/spec/lapis/yggdrasil/messaging/invalidate_request_spec.rb +29 -0
  51. data/spec/lapis/yggdrasil/messaging/refresh_request_spec.rb +70 -0
  52. data/spec/lapis/yggdrasil/messaging/refresh_response_spec.rb +50 -0
  53. data/spec/lapis/yggdrasil/messaging/response_factory_spec.rb +130 -0
  54. data/spec/lapis/yggdrasil/messaging/response_spec.rb +36 -0
  55. data/spec/lapis/yggdrasil/messaging/signout_request_spec.rb +29 -0
  56. data/spec/lapis/yggdrasil/messaging/validate_request_spec.rb +29 -0
  57. data/spec/lapis/yggdrasil/profile_spec.rb +108 -0
  58. data/spec/lapis/yggdrasil/session_info_spec.rb +131 -0
  59. data/spec/lapis/yggdrasil/session_spec.rb +158 -0
  60. data/spec/spec_helper.rb +89 -0
  61. metadata +269 -0
@@ -0,0 +1,14 @@
1
+ FactoryGirl.define do
2
+ factory :error, class: Lapis::Yggdrasil::AuthenticationError do
3
+ transient do
4
+ code Lapis::Yggdrasil::ErrorCodes::INVALID_CREDENTIALS
5
+ message 'Invalid credentials'
6
+ end
7
+
8
+ trait :no_message do
9
+ message nil
10
+ end
11
+
12
+ initialize_with { new(code, message) }
13
+ end
14
+ end
@@ -0,0 +1,111 @@
1
+ require 'json'
2
+
3
+ FactoryGirl.define do
4
+ factory :profile_entry, class: Hash do
5
+ transient do
6
+ profile { build(:profile) }
7
+ end
8
+
9
+ initialize_with do
10
+ hash = {
11
+ :name => profile.name,
12
+ :id => profile.id.to_s(false)
13
+ }
14
+ hash[:legacy] = true if profile.legacy?
15
+ hash
16
+ end
17
+ end
18
+
19
+ factory :message, class: HTTP::Message do
20
+ transient do
21
+ structure Hash.new
22
+ document { structure.to_json }
23
+ status 200
24
+ end
25
+
26
+ trait :empty do
27
+ document ''
28
+ end
29
+
30
+ initialize_with do
31
+ message = HTTP::Message.new_response(document)
32
+ message.status = status
33
+ message
34
+ end
35
+ end
36
+
37
+ factory :token_message, parent: :message do
38
+ transient do
39
+ access { build(:uuid) }
40
+ client { build(:uuid) }
41
+ base_structure do
42
+ {
43
+ :accessToken => access.to_s(false),
44
+ :clientToken => client.to_s(false)
45
+ }
46
+ end
47
+ structure { base_structure }
48
+ end
49
+ end
50
+
51
+ factory :error_message, parent: :message do
52
+ transient do
53
+ error 'Not Found'
54
+ error_message 'The server has not found anything matching the request URI'
55
+ cause 'Testing'
56
+ status 404
57
+ structure do
58
+ hash = {
59
+ :error => error,
60
+ :errorMessage => error_message
61
+ }
62
+ hash[:cause] = cause if cause
63
+ hash
64
+ end
65
+ end
66
+
67
+ trait :forbidden do
68
+ error 'ForbiddenOperationException'
69
+ error_message 'Invalid credentials. Invalid username or password.'
70
+ cause nil
71
+ end
72
+
73
+ trait :token do
74
+ error 'ForbiddenOperationException'
75
+ error_message 'Invalid token.'
76
+ cause nil
77
+ end
78
+
79
+ trait :profile do
80
+ error 'IllegalArgumentException'
81
+ error_message 'Access token already has a profile assigned.'
82
+ cause nil
83
+ end
84
+
85
+ trait :no_cause do
86
+ cause nil
87
+ end
88
+ end
89
+
90
+ factory :authentication_message, aliases: [:auth_message], parent: :token_message do
91
+ transient do
92
+ profiles { build_list(:profile, 5) }
93
+ selected { profiles.first }
94
+ structure do
95
+ base_structure.
96
+ merge(:availableProfiles => profiles.map { |profile| build(:profile_entry, :profile => profile) }).
97
+ merge(:selectedProfile => build(:profile_entry, :profile => selected))
98
+ end
99
+ end
100
+ end
101
+
102
+ factory :refresh_message, parent: :token_message do
103
+ transient do
104
+ profile { build(:profile) }
105
+ structure do
106
+ base_structure.
107
+ merge(:selectedProfile => build(:profile_entry, :profile => profile))
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,20 @@
1
+ FactoryGirl.define do
2
+ factory :profile, class: Lapis::Yggdrasil::Profile do
3
+ transient do
4
+ id nil
5
+ name 'Notch'
6
+ uuid { build(:uuid, :source => id) }
7
+ is_legacy false
8
+ end
9
+
10
+ trait :legacy do
11
+ is_legacy true
12
+ end
13
+
14
+ trait :modern do
15
+ is_legacy false
16
+ end
17
+
18
+ initialize_with { new(uuid, name, is_legacy) }
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ FactoryGirl.define do
2
+ factory :session_info, aliases: [:info], class: Lapis::Yggdrasil::SessionInfo do
3
+ transient do
4
+ client_token { build(:uuid) }
5
+ access_token { build(:uuid) }
6
+ profile { build(:profile) }
7
+ end
8
+
9
+ initialize_with { new(client_token, access_token, profile) }
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ require 'lapis/uuid'
2
+
3
+ FactoryGirl.define do
4
+ factory :uuid, class: Lapis::Uuid do
5
+ transient do
6
+ source nil
7
+ end
8
+
9
+ initialize_with do
10
+ case source
11
+ when nil
12
+ Lapis::Uuid.generate
13
+ when Lapis::Uuid
14
+ source
15
+ else
16
+ Lapis::Uuid.parse(source.to_s)
17
+ end
18
+ end
19
+ end
20
+
21
+ factory :uuid_str, class: String do
22
+ transient do
23
+ source nil
24
+ end
25
+
26
+ initialize_with { build(:uuid, :source => source).to_s.delete('-') }
27
+ end
28
+ end
@@ -0,0 +1,103 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ RSpec.describe Lapis::Yggdrasil::Agent do
4
+
5
+ describe '#name' do
6
+ let(:name) { 'Foobar' }
7
+ subject(:agent) { build(:agent, :name => name) }
8
+ subject { agent.name }
9
+
10
+ it 'is the expected value' do
11
+ is_expected.to eq name
12
+ end
13
+
14
+ it 'is frozen' do
15
+ is_expected.to be_frozen
16
+ end
17
+ end
18
+
19
+ describe '#version' do
20
+ let(:version) { 5 }
21
+ subject(:agent) { build(:agent, :version => version) }
22
+ subject { agent.version }
23
+
24
+ it 'is the expected value' do
25
+ is_expected.to eq version
26
+ end
27
+ end
28
+
29
+ describe '#==' do
30
+ subject { agent1 == agent2 }
31
+
32
+ context 'with identical agents' do
33
+ let(:agent1) { build(:agent) }
34
+ let(:agent2) { Lapis::Yggdrasil::Agent.new(agent1.name, agent1.version) }
35
+
36
+ it 'is true' do
37
+ is_expected.to eq true
38
+ end
39
+ end
40
+
41
+ context 'with different agents' do
42
+ let(:agent1) { build(:agent) }
43
+ let(:agent2) { Lapis::Yggdrasil::Agent.new(agent1.name + '-foobar', agent1.version) }
44
+
45
+ it 'is false' do
46
+ is_expected.to eq false
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '.from_properties' do
52
+ let(:name) { 'Foobar' }
53
+ let(:version) { 5 }
54
+
55
+ context 'with all properties set' do
56
+ subject(:agent) { Lapis::Yggdrasil::Agent.from_properties({ :name => name, :version => version }) }
57
+
58
+ it 'sets the name' do
59
+ expect(agent.name).to eq name
60
+ end
61
+
62
+ it 'sets the version' do
63
+ expect(agent.version).to eq version
64
+ end
65
+ end
66
+
67
+ context 'without the name property set' do
68
+ let(:default_name) { Lapis::Yggdrasil::Agent::DEFAULT_AGENT.name }
69
+ subject(:agent) { Lapis::Yggdrasil::Agent.from_properties({ :version => version }) }
70
+
71
+ it 'uses the default name' do
72
+ expect(agent.name).to eq default_name
73
+ end
74
+ end
75
+
76
+ context 'without the version property set' do
77
+ let(:default_version) { Lapis::Yggdrasil::Agent::DEFAULT_AGENT.version }
78
+ subject(:agent) { Lapis::Yggdrasil::Agent.from_properties({ :name => name }) }
79
+
80
+ it 'uses the default version' do
81
+ expect(agent.version).to eq default_version
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ describe '::DEFAULT_AGENT' do
88
+ subject(:agent) { Lapis::Yggdrasil::Agent::DEFAULT_AGENT }
89
+
90
+ it 'is an Agent instance' do
91
+ is_expected.to be_a Lapis::Yggdrasil::Agent
92
+ end
93
+
94
+ it 'has "Minecraft" as the name' do
95
+ expect(agent.name).to eq 'Minecraft'
96
+ end
97
+
98
+ it 'has 1 as the version' do
99
+ expect(agent.version).to eq 1
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,200 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ RSpec.describe Lapis::Yggdrasil::AuthenticationClient do
4
+ let(:username) { 'Notch' }
5
+ let(:password) { 'qwerty' }
6
+ let(:bad_username) { 'N0tch' }
7
+ let(:bad_password) { '1337h@x0r' }
8
+ let(:prev_session) { build(:info) }
9
+ let(:bad_session) { build(:info) }
10
+ let(:profile) { build(:profile) }
11
+
12
+ let(:url) { 'https://example.com' }
13
+ subject(:client) { Lapis::Yggdrasil::AuthenticationClient.new(url) }
14
+
15
+ before(:each) do
16
+ stub_request(:post, url + '/authenticate').
17
+ with(headers: { 'Content-Type' => 'application/json' }).to_return do |request|
18
+ hash = JSON.parse(request.body)
19
+ message = if hash['username'] == username && hash['password'] == password
20
+ client_token = hash.key?('clientToken') ? Lapis::Uuid.parse(hash['clientToken']) : prev_session.client_token
21
+ build(:authentication_message, :access => prev_session.access_token, :client => client_token)
22
+ else
23
+ build(:error_message, :forbidden)
24
+ end
25
+ { :body => message.body, :status => message.status, :headers => { 'Content-Type' => 'application/json' } }
26
+ end
27
+
28
+ stub_request(:post, url + '/refresh').
29
+ with(headers: { 'Content-Type' => 'application/json' }).to_return do |request|
30
+ hash = JSON.parse(request.body)
31
+ message = if prev_session.access_token == hash['accessToken'] && prev_session.client_token == hash['clientToken']
32
+ if hash.key?('selectedProfile')
33
+ build(:error_message, :profile)
34
+ else
35
+ build(:refresh_message, :client => prev_session.client_token)
36
+ end
37
+ else
38
+ build(:error_message, :token)
39
+ end
40
+ { :body => message.body, :status => message.status, :headers => { 'Content-Type' => 'application/json' } }
41
+ end
42
+
43
+ stub_request(:post, url + '/validate').
44
+ with(headers: { 'Content-Type' => 'application/json' }).to_return do |request|
45
+ hash = JSON.parse(request.body)
46
+ message = if prev_session.access_token == hash['accessToken'] && prev_session.client_token == hash['clientToken']
47
+ build(:message, :empty)
48
+ else
49
+ build(:error_message, :token)
50
+ end
51
+ { :body => message.body, :status => message.status, :headers => { 'Content-Type' => 'application/json' } }
52
+ end
53
+
54
+ stub_request(:post, url + '/signout').
55
+ with(headers: { 'Content-Type' => 'application/json' }).to_return do |request|
56
+ hash = JSON.parse(request.body)
57
+ message = if hash['username'] == username && hash['password'] == password
58
+ build(:message, :empty)
59
+ else
60
+ build(:error_message, :forbidden)
61
+ end
62
+ { :body => message.body, :status => message.status, :headers => { 'Content-Type' => 'application/json' } }
63
+ end
64
+
65
+ stub_request(:post, url + '/invalidate').
66
+ with(headers: { 'Content-Type' => 'application/json' }).to_return do |request|
67
+ hash = JSON.parse(request.body)
68
+ message = if prev_session.access_token == hash['accessToken'] && prev_session.client_token == hash['clientToken']
69
+ build(:message, :empty)
70
+ else
71
+ build(:error_message, :token)
72
+ end
73
+ { :body => message.body, :status => message.status, :headers => { 'Content-Type' => 'application/json' } }
74
+ end
75
+ end
76
+
77
+ describe '.official' do
78
+ subject { Lapis::Yggdrasil::AuthenticationClient.official }
79
+
80
+ it 'is an AuthenticationClient' do
81
+ is_expected.to be_an Lapis::Yggdrasil::AuthenticationClient
82
+ end
83
+ end
84
+
85
+ describe '#authenticate' do
86
+ context 'with valid credentials' do
87
+ subject(:session) { client.authenticate(username, password) }
88
+
89
+ it 'returns a Session' do
90
+ is_expected.to be_a Lapis::Yggdrasil::Session
91
+ end
92
+ end
93
+
94
+ context 'with invalid credentials' do
95
+ subject(:session) { client.authenticate(bad_username, bad_password) }
96
+
97
+ it 'raises an AuthenticationError' do
98
+ expect { subject }.to raise_error(Lapis::Yggdrasil::AuthenticationError)
99
+ end
100
+ end
101
+
102
+ context 'with a client token' do
103
+ let(:client_token) { build(:uuid) }
104
+ subject(:session) { client.authenticate(username, password, client_token) }
105
+
106
+ it 'retains the client token' do
107
+ expect(session.info.client_token).to eq client_token
108
+ end
109
+ end
110
+
111
+ context 'without a client token' do
112
+ subject(:session) { client.authenticate(username, password, nil) }
113
+
114
+ it 'gets a new client token' do
115
+ expect(session.info.client_token).not_to eq nil
116
+ end
117
+ end
118
+ end
119
+
120
+ describe '#refresh' do
121
+ context 'with a valid session' do
122
+ subject(:session) { client.refresh(prev_session) }
123
+
124
+ it 'returns updated session info' do
125
+ is_expected.to be_a Lapis::Yggdrasil::SessionInfo
126
+ end
127
+ end
128
+
129
+ context 'with an invalid session' do
130
+ subject(:session) { client.refresh(bad_session) }
131
+
132
+ it 'raises an AuthenticationError' do
133
+ expect { subject }.to raise_error(Lapis::Yggdrasil::AuthenticationError)
134
+ end
135
+ end
136
+
137
+ context 'with a profile' do
138
+ subject(:session) { client.refresh(prev_session, profile) }
139
+
140
+ it 'raises an AuthenticationError' do
141
+ expect { subject }.to raise_error(Lapis::Yggdrasil::AuthenticationError)
142
+ end
143
+ end
144
+ end
145
+
146
+ describe '#validate' do
147
+ context 'with a valid session' do
148
+ subject(:result) { client.validate(prev_session) }
149
+
150
+ it 'is true' do
151
+ is_expected.to eq true
152
+ end
153
+ end
154
+
155
+ context 'with an invalid session' do
156
+ subject(:result) { client.validate(bad_session) }
157
+
158
+ it 'is false' do
159
+ is_expected.to eq false
160
+ end
161
+ end
162
+ end
163
+
164
+ describe '#signout' do
165
+ context 'with valid credentials' do
166
+ subject { client.signout(username, password) }
167
+
168
+ it 'does not raise an error' do
169
+ expect { subject }.not_to raise_error
170
+ end
171
+ end
172
+
173
+ context 'with invalid credentials' do
174
+ subject { client.signout(bad_username, bad_password) }
175
+
176
+ it 'raises an AuthenticationError' do
177
+ expect { subject }.to raise_error(Lapis::Yggdrasil::AuthenticationError)
178
+ end
179
+ end
180
+ end
181
+
182
+ describe '#invalidate' do
183
+ context 'with a valid session' do
184
+ subject { client.invalidate(prev_session) }
185
+
186
+ it 'does not raise an error' do
187
+ expect { subject }.not_to raise_error
188
+ end
189
+ end
190
+
191
+ context 'with an invalid session' do
192
+ subject { client.invalidate(bad_session) }
193
+
194
+ it 'raises an AuthenticationError' do
195
+ expect { subject }.to raise_error(Lapis::Yggdrasil::AuthenticationError)
196
+ end
197
+ end
198
+ end
199
+
200
+ end