gitlab-mail_room 0.0.9 → 0.0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab/issue_templates/Default.md +9 -0
  3. data/.gitlab/issue_templates/Release.md +1 -0
  4. data/.gitlab-ci.yml +14 -24
  5. data/.rubocop.yml +5 -0
  6. data/.rubocop_todo.yml +494 -0
  7. data/.ruby-version +1 -1
  8. data/.travis.yml +12 -5
  9. data/CHANGELOG.md +4 -0
  10. data/CONTRIBUTING.md +40 -0
  11. data/README.md +125 -14
  12. data/Rakefile +1 -1
  13. data/lib/mail_room/arbitration/redis.rb +1 -1
  14. data/lib/mail_room/connection.rb +6 -1
  15. data/lib/mail_room/crash_handler.rb +2 -1
  16. data/lib/mail_room/delivery/letter_opener.rb +1 -1
  17. data/lib/mail_room/delivery/postback.rb +42 -6
  18. data/lib/mail_room/delivery/que.rb +15 -1
  19. data/lib/mail_room/delivery/sidekiq.rb +4 -3
  20. data/lib/mail_room/jwt.rb +39 -0
  21. data/lib/mail_room/logger/structured.rb +15 -1
  22. data/lib/mail_room/mailbox.rb +56 -17
  23. data/lib/mail_room/mailbox_watcher.rb +7 -1
  24. data/lib/mail_room/microsoft_graph/connection.rb +243 -0
  25. data/lib/mail_room/microsoft_graph.rb +7 -0
  26. data/lib/mail_room/version.rb +1 -1
  27. data/mail_room.gemspec +7 -1
  28. data/spec/fixtures/jwt_secret +1 -0
  29. data/spec/lib/arbitration/redis_spec.rb +6 -5
  30. data/spec/lib/cli_spec.rb +3 -3
  31. data/spec/lib/configuration_spec.rb +1 -1
  32. data/spec/lib/delivery/letter_opener_spec.rb +4 -3
  33. data/spec/lib/delivery/logger_spec.rb +3 -2
  34. data/spec/lib/delivery/postback_spec.rb +62 -14
  35. data/spec/lib/delivery/sidekiq_spec.rb +33 -11
  36. data/spec/lib/jwt_spec.rb +80 -0
  37. data/spec/lib/logger/structured_spec.rb +34 -2
  38. data/spec/lib/mailbox_spec.rb +65 -17
  39. data/spec/lib/mailbox_watcher_spec.rb +54 -38
  40. data/spec/lib/microsoft_graph/connection_spec.rb +252 -0
  41. data/spec/spec_helper.rb +14 -3
  42. metadata +97 -8
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'oauth2'
5
+
6
+ module MailRoom
7
+ module MicrosoftGraph
8
+ class Connection < MailRoom::Connection
9
+ NEXT_PAGE_KEY = '@odata.nextLink'
10
+ DEFAULT_POLL_INTERVAL_S = 60
11
+
12
+ TooManyRequestsError = Class.new(RuntimeError)
13
+
14
+ attr_accessor :token, :throttled_count
15
+
16
+ def initialize(mailbox)
17
+ super
18
+
19
+ reset
20
+ setup
21
+ end
22
+
23
+ def wait
24
+ return if stopped?
25
+
26
+ process_mailbox
27
+
28
+ @throttled_count = 0
29
+ wait_for_new_messages
30
+ rescue TooManyRequestsError => e
31
+ @throttled_count += 1
32
+
33
+ @mailbox.logger.warn({ context: @mailbox.context, action: 'Too many requests, backing off...', backoff_s: backoff_secs, error: e.message, error_backtrace: e.backtrace })
34
+
35
+ backoff
36
+ rescue IOError => e
37
+ @mailbox.logger.warn({ context: @mailbox.context, action: 'Disconnected. Resetting...', error: e.message, error_backtrace: e.backtrace })
38
+
39
+ reset
40
+ setup
41
+ end
42
+
43
+ private
44
+
45
+ def wait_for_new_messages
46
+ sleep_while_running(poll_interval)
47
+ end
48
+
49
+ def backoff
50
+ sleep_while_running(backoff_secs)
51
+ end
52
+
53
+ def backoff_secs
54
+ [60 * 10, 2**throttled_count].min
55
+ end
56
+
57
+ # Unless wake up periodically, we won't notice that the thread was stopped
58
+ # if we sleep the entire interval.
59
+ def sleep_while_running(sleep_interval)
60
+ sleep_interval.times do
61
+ do_sleep(1)
62
+ return if stopped?
63
+ end
64
+ end
65
+
66
+ def do_sleep(interval)
67
+ sleep(interval)
68
+ end
69
+
70
+ def reset
71
+ @token = nil
72
+ @throttled_count = 0
73
+ end
74
+
75
+ def setup
76
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Retrieving OAuth2 token...' })
77
+
78
+ @token = client.client_credentials.get_token({ scope: scope })
79
+ end
80
+
81
+ def client
82
+ @client ||= OAuth2::Client.new(client_id, client_secret,
83
+ site: azure_ad_endpoint,
84
+ authorize_url: "/#{tenant_id}/oauth2/v2.0/authorize",
85
+ token_url: "/#{tenant_id}/oauth2/v2.0/token",
86
+ auth_scheme: :basic_auth)
87
+ end
88
+
89
+ def inbox_options
90
+ mailbox.inbox_options
91
+ end
92
+
93
+ def tenant_id
94
+ inbox_options[:tenant_id]
95
+ end
96
+
97
+ def client_id
98
+ inbox_options[:client_id]
99
+ end
100
+
101
+ def client_secret
102
+ inbox_options[:client_secret]
103
+ end
104
+
105
+ def poll_interval
106
+ @poll_interval ||= begin
107
+ interval = inbox_options[:poll_interval].to_i
108
+
109
+ if interval.positive?
110
+ interval
111
+ else
112
+ DEFAULT_POLL_INTERVAL_S
113
+ end
114
+ end
115
+ end
116
+
117
+ def process_mailbox
118
+ return unless @new_message_handler
119
+
120
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Processing started' })
121
+
122
+ new_messages.each do |msg|
123
+ success = @new_message_handler.call(msg)
124
+ handle_delivered(msg) if success
125
+ end
126
+ end
127
+
128
+ def handle_delivered(msg)
129
+ mark_as_read(msg)
130
+ delete_message(msg) if @mailbox.delete_after_delivery
131
+ end
132
+
133
+ def delete_message(msg)
134
+ token.delete(msg_url(msg.uid))
135
+ end
136
+
137
+ def mark_as_read(msg)
138
+ token.patch(msg_url(msg.uid),
139
+ headers: { 'Content-Type' => 'application/json' },
140
+ body: { isRead: true }.to_json)
141
+ end
142
+
143
+ def new_messages
144
+ messages_for_ids(new_message_ids)
145
+ end
146
+
147
+ # Yields a page of message IDs at a time
148
+ def new_message_ids
149
+ url = unread_messages_url
150
+
151
+ Enumerator.new do |block|
152
+ loop do
153
+ messages, next_page_url = unread_messages(url: url)
154
+ messages.each { |msg| block.yield msg }
155
+
156
+ break unless next_page_url
157
+
158
+ url = next_page_url
159
+ end
160
+ end
161
+ end
162
+
163
+ def unread_messages(url:)
164
+ body = get(url)
165
+
166
+ return [[], nil] unless body
167
+
168
+ all_unread = body['value'].map { |msg| msg['id'] }
169
+ to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) }
170
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Getting new messages',
171
+ unread: { count: all_unread.count, ids: all_unread },
172
+ to_be_delivered: { count: to_deliver.count, ids: to_deliver } })
173
+ [to_deliver, body[NEXT_PAGE_KEY]]
174
+ rescue TypeError, JSON::ParserError => e
175
+ log_exception('Error parsing JSON response', e)
176
+ [[], nil]
177
+ end
178
+
179
+ # Returns the JSON response
180
+ def get(url)
181
+ response = token.get(url, { raise_errors: false })
182
+
183
+ # https://docs.microsoft.com/en-us/graph/errors
184
+ case response.status
185
+ when 509, 429
186
+ raise TooManyRequestsError
187
+ when 400..599
188
+ raise OAuth2::Error, response
189
+ end
190
+
191
+ return unless response.body
192
+
193
+ body = JSON.parse(response.body)
194
+
195
+ raise TypeError, 'Response did not contain value hash' unless body.is_a?(Hash) && body.key?('value')
196
+
197
+ body
198
+ end
199
+
200
+ def messages_for_ids(message_ids)
201
+ message_ids.each_with_object([]) do |id, arr|
202
+ response = token.get(rfc822_msg_url(id))
203
+
204
+ arr << ::MailRoom::Message.new(uid: id, body: response.body)
205
+ end
206
+ end
207
+
208
+ def base_url
209
+ "#{graph_endpoint}/v1.0/users/#{mailbox.email}/mailFolders/#{mailbox.name}/messages"
210
+ end
211
+
212
+ def unread_messages_url
213
+ "#{base_url}?$filter=isRead eq false"
214
+ end
215
+
216
+ def msg_url(id)
217
+ # Attempting to use the base_url fails with "The OData request is not supported"
218
+ "#{graph_endpoint}/v1.0/users/#{mailbox.email}/messages/#{id}"
219
+ end
220
+
221
+ def rfc822_msg_url(id)
222
+ # Attempting to use the base_url fails with "The OData request is not supported"
223
+ "#{msg_url(id)}/$value"
224
+ end
225
+
226
+ def log_exception(message, exception)
227
+ @mailbox.logger.warn({ context: @mailbox.context, message: message, exception: exception.to_s })
228
+ end
229
+
230
+ def scope
231
+ "#{graph_endpoint}/.default"
232
+ end
233
+
234
+ def graph_endpoint
235
+ inbox_options[:graph_endpoint] || 'https://graph.microsoft.com'
236
+ end
237
+
238
+ def azure_ad_endpoint
239
+ inbox_options[:azure_ad_endpoint] || 'https://login.microsoftonline.com'
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailRoom
4
+ module MicrosoftGraph
5
+ autoload :Connection, 'mail_room/microsoft_graph/connection'
6
+ end
7
+ end
@@ -1,4 +1,4 @@
1
1
  module MailRoom
2
2
  # Current version of gitlab-mail_room gem
3
- VERSION = "0.0.9"
3
+ VERSION = "0.0.23"
4
4
  end
data/mail_room.gemspec CHANGED
@@ -17,8 +17,13 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
+ gem.add_dependency "net-imap", ">= 0.2.1"
21
+ gem.add_dependency "oauth2", [">= 1.4.4", "< 3"]
22
+ gem.add_dependency "jwt", ">= 2.0"
23
+
20
24
  gem.add_development_dependency "rake"
21
25
  gem.add_development_dependency "rspec", "~> 3.9"
26
+ gem.add_development_dependency "rubocop", "~> 1.11"
22
27
  gem.add_development_dependency "mocha", "~> 1.11"
23
28
  gem.add_development_dependency "simplecov"
24
29
  gem.add_development_dependency "webrick", "~> 1.6"
@@ -27,8 +32,9 @@ Gem::Specification.new do |gem|
27
32
  gem.add_development_dependency "faraday"
28
33
  gem.add_development_dependency "mail"
29
34
  gem.add_development_dependency "letter_opener"
30
- gem.add_development_dependency "redis", "~> 3.3.1"
35
+ gem.add_development_dependency "redis", "~> 4"
31
36
  gem.add_development_dependency "redis-namespace"
32
37
  gem.add_development_dependency "pg"
33
38
  gem.add_development_dependency "charlock_holmes"
39
+ gem.add_development_dependency "webmock"
34
40
  end
@@ -0,0 +1 @@
1
+ aGVsbG93b3JsZA==
@@ -15,6 +15,7 @@ describe MailRoom::Arbitration::Redis do
15
15
 
16
16
  # Private, but we don't care.
17
17
  let(:redis) { subject.send(:client) }
18
+ let(:raw_client) { redis._client }
18
19
 
19
20
  describe '#deliver?' do
20
21
  context "when called the first time" do
@@ -95,7 +96,7 @@ describe MailRoom::Arbitration::Redis do
95
96
  it 'client has same specified url' do
96
97
  subject.deliver?(123)
97
98
 
98
- expect(redis.client.options[:url]).to eq redis_url
99
+ expect(raw_client.options[:url]).to eq redis_url
99
100
  end
100
101
 
101
102
  it 'client is a instance of Redis class' do
@@ -137,10 +138,10 @@ describe MailRoom::Arbitration::Redis do
137
138
  before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) }
138
139
 
139
140
  it 'client has same specified sentinel params' do
140
- expect(redis.client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
141
- expect(redis.client.options[:host]).to eq('sentinel-master')
142
- expect(redis.client.options[:password]).to eq('mypassword')
143
- expect(redis.client.options[:sentinels]).to eq(sentinels)
141
+ expect(raw_client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
142
+ expect(raw_client.options[:host]).to eq('sentinel-master')
143
+ expect(raw_client.options[:password]).to eq('mypassword')
144
+ expect(raw_client.options[:sentinels]).to eq(sentinels)
144
145
  end
145
146
  end
146
147
  end
data/spec/lib/cli_spec.rb CHANGED
@@ -2,8 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe MailRoom::CLI do
4
4
  let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
5
- let!(:configuration) {MailRoom::Configuration.new({:config_path => config_path})}
6
- let(:coordinator) {stub(:run => true, :quit => true)}
5
+ let!(:configuration) {MailRoom::Configuration.new({config_path: config_path})}
6
+ let(:coordinator) {stub(run: true, quit: true)}
7
7
  let(:configuration_args) { anything }
8
8
  let(:coordinator_args) { [anything, anything] }
9
9
 
@@ -17,7 +17,7 @@ describe MailRoom::CLI do
17
17
 
18
18
  context 'with configuration args' do
19
19
  let(:configuration_args) do
20
- {:config_path => 'a path'}
20
+ {config_path: 'a path'}
21
21
  end
22
22
 
23
23
  it 'parses arguments into configuration' do
@@ -5,7 +5,7 @@ describe MailRoom::Configuration do
5
5
 
6
6
  describe '#initalize' do
7
7
  context 'with config_path' do
8
- let(:configuration) { MailRoom::Configuration.new(:config_path => config_path) }
8
+ let(:configuration) { MailRoom::Configuration.new(config_path: config_path) }
9
9
 
10
10
  it 'parses yaml into mailbox objects' do
11
11
  MailRoom::Mailbox.stubs(:new).returns('mailbox1', 'mailbox2')
@@ -3,17 +3,18 @@ require 'mail_room/delivery/letter_opener'
3
3
 
4
4
  describe MailRoom::Delivery::LetterOpener do
5
5
  describe '#deliver' do
6
- let(:mailbox) {build_mailbox(:location => '/tmp/somewhere')}
7
- let(:delivery_method) {stub(:deliver!)}
6
+ let(:mailbox) {build_mailbox(location: '/tmp/somewhere')}
7
+ let(:delivery_method) {stub}
8
8
  let(:mail) {stub}
9
9
 
10
10
  before :each do
11
11
  Mail.stubs(:read_from_string).returns(mail)
12
12
  ::LetterOpener::DeliveryMethod.stubs(:new).returns(delivery_method)
13
+ delivery_method.stubs(:deliver!)
13
14
  end
14
15
 
15
16
  it 'creates a new LetterOpener::DeliveryMethod' do
16
- ::LetterOpener::DeliveryMethod.expects(:new).with(:location => '/tmp/somewhere').returns(delivery_method)
17
+ ::LetterOpener::DeliveryMethod.expects(:new).with(location: '/tmp/somewhere').returns(delivery_method)
17
18
 
18
19
  MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message')
19
20
  end
@@ -16,10 +16,11 @@ describe MailRoom::Delivery::Logger do
16
16
  end
17
17
 
18
18
  context "with a log path" do
19
- let(:mailbox) {build_mailbox(:log_path => '/var/log/mail-room.log')}
19
+ let(:mailbox) {build_mailbox(log_path: '/var/log/mail-room.log')}
20
20
 
21
21
  it 'creates a new file to append to' do
22
- file = stub(:sync=)
22
+ file = stub
23
+ file.stubs(:sync=)
23
24
 
24
25
  File.expects(:open).with('/var/log/mail-room.log', 'a').returns(file)
25
26
  ::Logger.stubs(:new).with(file)
@@ -5,8 +5,8 @@ describe MailRoom::Delivery::Postback do
5
5
  describe '#deliver' do
6
6
  context 'with token auth delivery' do
7
7
  let(:mailbox) {build_mailbox({
8
- :delivery_url => 'http://localhost/inbox',
9
- :delivery_token => 'abcdefg'
8
+ delivery_url: 'http://localhost/inbox',
9
+ delivery_token: 'abcdefg'
10
10
  })}
11
11
 
12
12
  let(:delivery_options) {
@@ -30,10 +30,10 @@ describe MailRoom::Delivery::Postback do
30
30
 
31
31
  context 'with basic auth delivery options' do
32
32
  let(:mailbox) {build_mailbox({
33
- :delivery_options => {
34
- :url => 'http://localhost/inbox',
35
- :username => 'user1',
36
- :password => 'password123abc'
33
+ delivery_options: {
34
+ url: 'http://localhost/inbox',
35
+ username: 'user1',
36
+ password: 'password123abc'
37
37
  }
38
38
  })}
39
39
 
@@ -57,19 +57,18 @@ describe MailRoom::Delivery::Postback do
57
57
 
58
58
  context 'with content type in the delivery options' do
59
59
  let(:mailbox) {build_mailbox({
60
- :delivery_options => {
61
- :url => 'http://localhost/inbox',
62
- :username => 'user1',
63
- :password => 'password123abc',
64
- :content_type => 'text/plain'
60
+ delivery_options: {
61
+ url: 'http://localhost/inbox',
62
+ username: 'user1',
63
+ password: 'password123abc',
64
+ content_type: 'text/plain'
65
65
  }
66
66
  })}
67
67
 
68
-
69
68
  let(:delivery_options) {
70
69
  MailRoom::Delivery::Postback::Options.new(mailbox)
71
70
  }
72
-
71
+
73
72
  it 'posts the message with faraday' do
74
73
  connection = stub
75
74
  request = stub
@@ -82,10 +81,59 @@ describe MailRoom::Delivery::Postback do
82
81
  connection.expects(:basic_auth).with('user1', 'password123abc')
83
82
 
84
83
  MailRoom::Delivery::Postback.new(delivery_options).deliver('a message')
85
-
84
+
86
85
  expect(request.headers['Content-Type']).to eq('text/plain')
87
86
  end
88
87
  end
88
+
89
+ context 'with jwt token in the delivery options' do
90
+ let(:mailbox) {build_mailbox({
91
+ delivery_options: {
92
+ url: 'http://localhost/inbox',
93
+ jwt_auth_header: "Mailroom-Api-Request",
94
+ jwt_issuer: "mailroom",
95
+ jwt_algorithm: "HS256",
96
+ jwt_secret_path: "secret_path"
97
+ }
98
+ })}
99
+
100
+ let(:delivery_options) {
101
+ MailRoom::Delivery::Postback::Options.new(mailbox)
102
+ }
103
+
104
+ it 'posts the message with faraday' do
105
+ connection = stub
106
+ request = stub
107
+ Faraday.stubs(:new).returns(connection)
108
+
109
+ connection.expects(:post).yields(request).twice
110
+ request.stubs(:url)
111
+ request.stubs(:body=)
112
+ request.stubs(:headers).returns({})
113
+
114
+ jwt = stub
115
+ MailRoom::JWT.expects(:new).with(
116
+ header: 'Mailroom-Api-Request',
117
+ issuer: 'mailroom',
118
+ algorithm: 'HS256',
119
+ secret_path: 'secret_path'
120
+ ).returns(jwt)
121
+ jwt.stubs(:valid?).returns(true)
122
+ jwt.stubs(:header).returns('Mailroom-Api-Request')
123
+ jwt.stubs(:token).returns('a_jwt_token')
124
+
125
+ delivery = MailRoom::Delivery::Postback.new(delivery_options)
126
+
127
+ delivery.deliver('a message')
128
+ expect(request.headers['Mailroom-Api-Request']).to eql('a_jwt_token')
129
+
130
+ # A different jwt token for the second time
131
+ jwt.stubs(:token).returns('another_jwt_token')
132
+
133
+ delivery.deliver('another message')
134
+ expect(request.headers['Mailroom-Api-Request']).to eql('another_jwt_token')
135
+ end
136
+ end
89
137
  end
90
138
  end
91
139
  end
@@ -4,27 +4,49 @@ require 'mail_room/delivery/sidekiq'
4
4
  describe MailRoom::Delivery::Sidekiq do
5
5
  subject { described_class.new(options) }
6
6
  let(:redis) { subject.send(:client) }
7
+ let(:raw_client) { redis._client }
7
8
  let(:options) { MailRoom::Delivery::Sidekiq::Options.new(mailbox) }
8
9
 
9
10
  describe '#options' do
10
11
  let(:redis_url) { 'redis://localhost' }
12
+ let(:redis_options) { { redis_url: redis_url } }
11
13
 
12
14
  context 'when only redis_url is specified' do
13
15
  let(:mailbox) {
14
16
  build_mailbox(
15
17
  delivery_method: :sidekiq,
16
- delivery_options: {
17
- redis_url: redis_url
18
- }
18
+ delivery_options: redis_options
19
19
  )
20
20
  }
21
21
 
22
- it 'client has same specified redis_url' do
23
- expect(redis.client.options[:url]).to eq(redis_url)
22
+ context 'with simple redis url' do
23
+ it 'client has same specified redis_url' do
24
+ expect(raw_client.options[:url]).to eq(redis_url)
25
+ end
26
+
27
+ it 'client is a instance of RedisNamespace class' do
28
+ expect(redis).to be_a ::Redis
29
+ end
30
+
31
+ it 'connection has correct values' do
32
+ expect(redis.connection[:host]).to eq('localhost')
33
+ expect(redis.connection[:db]).to eq(0)
34
+ end
24
35
  end
25
36
 
26
- it 'client is a instance of RedisNamespace class' do
27
- expect(redis).to be_a ::Redis
37
+ context 'with redis_db specified in options' do
38
+ before do
39
+ redis_options[:redis_db] = 4
40
+ end
41
+
42
+ it 'client has correct redis_url' do
43
+ expect(raw_client.options[:url]).to eq(redis_url)
44
+ end
45
+
46
+ it 'connection has correct values' do
47
+ expect(redis.connection[:host]).to eq('localhost')
48
+ expect(redis.connection[:db]).to eq(4)
49
+ end
28
50
  end
29
51
  end
30
52
 
@@ -65,10 +87,10 @@ describe MailRoom::Delivery::Sidekiq do
65
87
  before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) }
66
88
 
67
89
  it 'client has same specified sentinel params' do
68
- expect(redis.client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
69
- expect(redis.client.options[:host]).to eq('sentinel-master')
70
- expect(redis.client.options[:password]).to eq('mypassword')
71
- expect(redis.client.options[:sentinels]).to eq(sentinels)
90
+ expect(raw_client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
91
+ expect(raw_client.options[:host]).to eq('sentinel-master')
92
+ expect(raw_client.options[:password]).to eq('mypassword')
93
+ expect(raw_client.options[:sentinels]).to eq(sentinels)
72
94
  end
73
95
  end
74
96
 
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mail_room/jwt'
4
+
5
+ describe MailRoom::JWT do
6
+ let(:secret_path) { File.expand_path('../fixtures/jwt_secret', File.dirname(__FILE__)) }
7
+ let(:secret) { Base64.strict_decode64(File.read(secret_path).chomp) }
8
+
9
+ let(:standard_config) do
10
+ {
11
+ secret_path: secret_path,
12
+ issuer: 'mailroom',
13
+ header: 'Mailroom-Api-Request',
14
+ algorithm: 'HS256'
15
+ }
16
+ end
17
+
18
+ describe '#token' do
19
+ let(:jwt) { described_class.new(**standard_config) }
20
+
21
+ it 'generates a valid jwt token' do
22
+ token = jwt.token
23
+ expect(token).not_to be_empty
24
+
25
+ payload = nil
26
+ expect do
27
+ payload = JWT.decode(token, secret, true, iss: 'mailroom', verify_iat: true, verify_iss: true, algorithm: 'HS256')
28
+ end.not_to raise_error
29
+ expect(payload).to be_an(Array)
30
+ expect(payload).to match(
31
+ [
32
+ a_hash_including(
33
+ 'iss' => 'mailroom',
34
+ 'nonce' => be_a(String),
35
+ 'iat' => be_a(Integer)
36
+ ),
37
+ { 'alg' => 'HS256' }
38
+ ]
39
+ )
40
+ end
41
+
42
+ it 'generates a different token for each invocation' do
43
+ expect(jwt.token).not_to eql(jwt.token)
44
+ end
45
+ end
46
+
47
+ describe '#valid?' do
48
+ it 'returns true if all essential components are present' do
49
+ jwt = described_class.new(**standard_config)
50
+ expect(jwt.valid?).to eql(true)
51
+ end
52
+
53
+ it 'returns true if header and secret path are present' do
54
+ jwt = described_class.new(
55
+ secret_path: secret_path,
56
+ header: 'Mailroom-Api-Request',
57
+ issuer: nil,
58
+ algorithm: nil
59
+ )
60
+ expect(jwt.valid?).to eql(true)
61
+ expect(jwt.issuer).to eql(described_class::DEFAULT_ISSUER)
62
+ expect(jwt.algorithm).to eql(described_class::DEFAULT_ALGORITHM)
63
+ end
64
+
65
+ it 'returns false if either header or secret_path are missing' do
66
+ expect(described_class.new(
67
+ secret_path: nil,
68
+ header: 'Mailroom-Api-Request',
69
+ issuer: nil,
70
+ algorithm: nil
71
+ ).valid?).to eql(false)
72
+ expect(described_class.new(
73
+ secret_path: secret_path,
74
+ header: nil,
75
+ issuer: nil,
76
+ algorithm: nil
77
+ ).valid?).to eql(false)
78
+ end
79
+ end
80
+ end