keystok 1.0.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.
@@ -0,0 +1,605 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under GitDock Oy license terms.
3
+ # See https://bitbucket.org/keystok/keystok-client-ruby/raw/master/LICENSE.txt
4
+ # for details.
5
+
6
+ require 'spec_helper'
7
+ require 'fileutils'
8
+
9
+ describe Keystok::Client do
10
+ DECRYPTION_KEY =
11
+ '965391f2bba37b4586431b8690d7044d5cdc73adf7dda539f8dd3a60cb3e9b12'
12
+ API_HOST = 'https://api.keystok.com'
13
+
14
+ def request_url(host = nil, path = nil)
15
+ host ||= config[:api_host]
16
+ path ||= request_path
17
+ "#{host}/#{path}"
18
+ end
19
+
20
+ def override_config_key(key, value = nil)
21
+ config_tmp = client.instance_variable_get('@config')
22
+ config_tmp[key] = value
23
+ client.instance_variable_set('@config', config_tmp)
24
+ end
25
+
26
+ def request_path(key = nil)
27
+ if key
28
+ key_path = "/#{key}"
29
+ else
30
+ key_path = ''
31
+ end
32
+ "apps/#{config[:id]}/deploy#{key_path}?access_token=#{refreshed_token}"
33
+ end
34
+
35
+ def stub_deployment_req(host = nil, path = nil)
36
+ stub_request(:get, request_url(host, path))
37
+ .to_return(status: 200, body: example_response, headers: {})
38
+ end
39
+
40
+ let(:config) do
41
+ { api_host: 'https://api.keystok-host.com',
42
+ auth_host: 'https://keystok-host.com',
43
+ rt: 'token',
44
+ dk: DECRYPTION_KEY,
45
+ id: 1,
46
+ tmp_dir: tmp_dir }
47
+ end
48
+ let(:client) { Keystok::Client.new(config) }
49
+ let(:example_response) do
50
+ File.open(File.expand_path('../../fixtures/response_data_00.data',
51
+ __FILE__)).read
52
+ end
53
+ let(:keystok_oauth_refresh_url) { 'https://keystok.com/oauth/token' }
54
+ let(:oauth_refresh_url) { "#{config[:auth_host]}/oauth/token" }
55
+ let(:refresh_response) do
56
+ {
57
+ 'access_token' => refreshed_token,
58
+ 'token_type' => 'bearer', 'expires_in' => 124
59
+ }.to_json
60
+ end
61
+ let(:refreshed_token) do
62
+ '2cb4583cd8a399f51db15da57d0679706ae975d2aa9d3c71f5fdd0255dd6a028'
63
+ end
64
+ let(:tmp_dir) { File.expand_path('../../../tmp', __FILE__) }
65
+ let(:tmp_filepath) { File.join(tmp_dir, 'keystok_cache.data') }
66
+
67
+ before do
68
+ Keystok.logger = Logger.new(nil)
69
+ # config based url's
70
+ stub_deployment_req(config[:api_host], request_path)
71
+ stub_request(:post, oauth_refresh_url)
72
+ .to_return(status: 200, body: refresh_response,
73
+ headers: { 'Content-Type' => 'application/json' })
74
+ # Keystok URL's
75
+ stub_deployment_req(API_HOST, request_path)
76
+ stub_request(:post, keystok_oauth_refresh_url)
77
+ .to_return(status: 200, body: refresh_response,
78
+ headers: { 'Content-Type' => 'application/json' })
79
+ end
80
+
81
+ it 'allow initialization with empty config' do
82
+ Keystok::Client.new
83
+ Keystok::Client.new('')
84
+ Keystok::Client.new(connection_string: '', tmp_dir: 'tmp')
85
+ end
86
+
87
+ describe 'default for' do
88
+ it "'eager_fetching' if true" do
89
+ expect(client.instance_variable_get('@config')[:eager_fetching])
90
+ .to be_true
91
+ end
92
+ end
93
+
94
+ describe 'access_token' do
95
+ it 'perform request to auth_host' do
96
+ client.send(:access_token)
97
+ a_request(:post, oauth_refresh_url).should have_been_made.once
98
+ end
99
+
100
+ it 'respond with new token' do
101
+ token = client.send(:access_token)
102
+ expect(token.class).to eq(OAuth2::AccessToken)
103
+ expect(token.token.length).to be > 0
104
+ end
105
+
106
+ it 'cached the token' do
107
+ token_1 = client.send(:access_token)
108
+ token_2 = client.send(:access_token)
109
+ expect(token_2).to equal(token_1)
110
+ end
111
+
112
+ it 'raise ConfigError when refresh token is nil' do
113
+ override_config_key(:rt, nil)
114
+ expect do
115
+ client.send(:access_token)
116
+ end.to raise_error(Keystok::Error::ConfigError, 'SDK not configured')
117
+ end
118
+
119
+ context 'connection fail' do
120
+ context 'Faraday exception' do
121
+ it 'tries REQUEST_TRY_COUNT times before giving-up' do
122
+ stub_request(:post, oauth_refresh_url)
123
+ .to_raise(Faraday::Error::ConnectionFailed)
124
+ begin
125
+ client.send(:access_token)
126
+ # rubocop:disable HandleExceptions
127
+ rescue Faraday::Error::ConnectionFailed
128
+ # rubocop:enable HandleExceptions
129
+ # Nothing here becouse we just want to check requests count
130
+ end
131
+ a_request(:post, oauth_refresh_url).should have_been_made
132
+ .times(Keystok::Client::REQUEST_TRY_COUNT)
133
+ end
134
+
135
+ it 'is re-raising Faraday exception after 3 attempts' do
136
+ stub_request(:post, oauth_refresh_url)
137
+ .to_raise(Faraday::Error::ConnectionFailed)
138
+ expect do
139
+ client.send(:access_token)
140
+ end.to raise_error(Faraday::Error::ConnectionFailed)
141
+ end
142
+
143
+ it 'should log warning on every Faraday exception' do
144
+ stub_request(:post, oauth_refresh_url)
145
+ .to_raise(Faraday::Error::ConnectionFailed)
146
+ logger = double(:logger)
147
+ Keystok.logger = logger
148
+ expect(logger).to receive(:warn).exactly(3).times
149
+ .with('Exception during token refresh: ' \
150
+ 'Exception from WebMock')
151
+ begin
152
+ client.send(:access_token)
153
+ # rubocop:disable HandleExceptions
154
+ rescue Faraday::Error::ConnectionFailed
155
+ # rubocop:enable HandleExceptions
156
+ # Nothing here becouse we just want to check requests count
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ describe 'configured?' do
164
+ conf_keys = %w(rt dk id).map(&:to_sym)
165
+ conf_keys.each do |config_key|
166
+ it "return false when '#{config_key}' entry is missing" do
167
+ override_config_key(config_key)
168
+ expect(client.send(:configured?)).to be_false
169
+ end
170
+ end
171
+
172
+ it 'return true when all required keys are set' do
173
+ expect(client.send(:configured?)).to be_true
174
+ end
175
+ end
176
+
177
+ describe 'get' do
178
+ it 'respond with proper value' do
179
+ expect(client.get('key_1')).to eq('Value 1')
180
+ end
181
+
182
+ it 'accept symbols' do
183
+ expect(client.get(:key_1)).to eq('Value 1')
184
+ end
185
+
186
+ it "don't trigger API request when key is in keys store" do
187
+ 2.times { client.get('key_1') }
188
+ a_request(:get, request_url).should have_been_made.once
189
+ end
190
+
191
+ it 'triggers API request when key is not in keys store' do
192
+ client.get('key_1')
193
+ a_request(:get, request_url).should have_been_made.once
194
+ end
195
+
196
+ it "don't trigger API request when key is in keys store" do
197
+ 2.times.each { client.get('key_1') }
198
+ a_request(:get, request_url).should have_been_made.once
199
+ end
200
+
201
+ it "triggers API request when 'force_reload' is true" do
202
+ 2.times.each { client.get('key_1', true) }
203
+ a_request(:get, request_url).should have_been_made.times(2)
204
+ end
205
+
206
+ it "triggers API request when 'volatile' is set in config" do
207
+ override_config_key(:volatile, true)
208
+ 2.times.each { client.get('key_1') }
209
+ a_request(:get, request_url).should have_been_made.times(2)
210
+ end
211
+
212
+ it "triggers request to 'https://api.keystok.com' when api_host is nil" do
213
+ override_config_key(:api_host)
214
+ client.get('key_1')
215
+ a_request(:get, request_url(API_HOST))
216
+ .should have_been_made.once
217
+ end
218
+ end
219
+
220
+ describe 'keys' do
221
+ before do
222
+ stub_request(:get, request_url)
223
+ .to_return(status: 200, body: example_response, headers: {})
224
+ end
225
+
226
+ it 'respond with proper values' do
227
+ expect(client.keys).to eq('key_1' => 'Value 1', 'key_2' => 'Value 2')
228
+ end
229
+
230
+ it 'triggers API request when keys store is empty' do
231
+ client.keys
232
+ a_request(:get, request_url).should have_been_made.once
233
+ end
234
+
235
+ it "don't trigger API request when keys store is not empty" do
236
+ 2.times { client.keys }
237
+ a_request(:get, request_url).should have_been_made.once
238
+ end
239
+
240
+ it "don't trigger API request when keys store is not empty" do
241
+ 2.times.each { client.keys }
242
+ a_request(:get, request_url).should have_been_made.once
243
+ end
244
+
245
+ it "triggers API request when 'force_reload' is true" do
246
+ 2.times.each { client.keys(true) }
247
+ a_request(:get, request_url).should have_been_made.times(2)
248
+ end
249
+
250
+ it "triggers API request when 'volatile' is set in config" do
251
+ override_config_key(:volatile, true)
252
+ 2.times.each { client.keys(true) }
253
+ a_request(:get, request_url).should have_been_made.times(2)
254
+ end
255
+ end
256
+
257
+ describe 'check_config' do
258
+ conf_keys = %w(rt dk id).map(&:to_sym)
259
+ conf_keys.each do |config_key|
260
+ it "raise ConfigError when '#{config_key}' entry is missing" do
261
+ override_config_key(config_key)
262
+ expect do
263
+ client.send(:check_config)
264
+ end.to raise_error(Keystok::Error::ConfigError,
265
+ "Config key: #{config_key} is missing!")
266
+ end
267
+ end
268
+ end
269
+
270
+ describe 'connection' do
271
+ it 'initialize Faraday instance' do
272
+ connection = client.send(:connection)
273
+ expect(connection.url_prefix.to_s).to eq("#{config[:api_host]}/")
274
+ end
275
+
276
+ it "is using 'https://api.keystok.com' when api_host is nil" do
277
+ override_config_key(:api_host)
278
+ connection = client.send(:connection)
279
+ expect(connection.url_prefix.to_s).to eq('https://api.keystok.com/')
280
+ end
281
+
282
+ it 'SSL verification is enabled' do
283
+ connection = client.send(:connection)
284
+ expect(connection.ssl.verify?).to eq(true)
285
+ end
286
+
287
+ it 'caches the instance' do
288
+ connection_1 = client.send(:connection)
289
+ connection_2 = client.send(:connection)
290
+ expect(connection_2).to equal(connection_1)
291
+ end
292
+ end
293
+
294
+ context 'config parsing' do
295
+ let(:config_hash) { { 'api_host' => 'http://dum.my', 'some_data' => 2 } }
296
+ describe 'decode_config' do
297
+ it "returns input if it's a hash" do
298
+ expect(client.send(:decode_config, config_hash)).to eq(config_hash)
299
+ end
300
+
301
+ it 'decodes connection string and merges it to the config hash' do
302
+ config_with_connection_string = {
303
+ connection_string: Base64.urlsafe_encode64(config_hash.to_json)
304
+ }
305
+ expect(client.send(:decode_config,
306
+ config_with_connection_string)).to eq(config_hash)
307
+ end
308
+
309
+ it "raise ConfigError when connection string can't be decoded" do
310
+ config_with_connection_string = {
311
+ connection_string: Base64.urlsafe_encode64(config_hash.to_json)
312
+ }
313
+ expect(client.send(:decode_config,
314
+ config_with_connection_string)).to eq(config_hash)
315
+ end
316
+
317
+ it "returns hash if it's a JSON string" do
318
+ expect(client.send(:decode_config,
319
+ config_hash.to_json)).to eq(config_hash)
320
+ end
321
+
322
+ it "returns hash if it's a base64 encoded JSON string" do
323
+ encoded_hash = Base64.urlsafe_encode64(config_hash.to_json)
324
+ expect(client.send(:decode_config,
325
+ encoded_hash.to_json)).to eq(config_hash)
326
+ end
327
+
328
+ it 'raise ConfigError exception on unknown input type' do
329
+ expect do
330
+ client.send(:decode_config, [:some, :array])
331
+ end.to raise_error(Keystok::Error::ConfigError,
332
+ 'Unknown config format')
333
+ end
334
+ end
335
+
336
+ describe 'decode_json_string_config' do
337
+ it 'respond with hash on JSON string' do
338
+ expect(client.send(:decode_json_string_config,
339
+ config_hash.to_json)).to eq(config_hash)
340
+ end
341
+
342
+ it 'responds with nil when parsing JSON is failing' do
343
+ expect(client.send(:decode_json_string_config,
344
+ 'not_valid_json')).to eq(nil)
345
+ end
346
+ end
347
+
348
+ describe 'decode_string_config' do
349
+ it "returns hash if it's a JSON string" do
350
+ expect(client.send(:decode_string_config,
351
+ config_hash.to_json)).to eq(config_hash)
352
+ end
353
+
354
+ it "returns hash if it's a base64 encoded JSON string" do
355
+ encoded_hash = Base64.urlsafe_encode64(config_hash.to_json)
356
+ expect(client.send(:decode_string_config,
357
+ encoded_hash.to_json)).to eq(config_hash)
358
+ end
359
+
360
+ it 'raise ConfigError exception on unknown input type' do
361
+ expect do
362
+ client.send(:decode_string_config, 'not_valid_json')
363
+ end.to raise_error(Keystok::Error::ConfigError,
364
+ 'Unknown config format')
365
+ end
366
+
367
+ it 'accepts empty string' do
368
+ expect(client.send(:decode_string_config, '')).to eq({})
369
+ end
370
+ end
371
+ end
372
+
373
+ describe 'fetch_data' do
374
+ context 'connection success' do
375
+ before do
376
+ stub_request(:get, request_url)
377
+ .to_return(status: 200, body: example_response, headers: {})
378
+ end
379
+
380
+ it 'is making request to proper url' do
381
+ client.send(:fetch_data)
382
+ a_request(:get, request_url).should have_been_made.once
383
+ end
384
+
385
+ context 'is making request to proper url when key is not nil' do
386
+ it 'and @config[:eager_fetching] is true' do
387
+ client.send(:fetch_data, :some_key)
388
+ a_request(:get, request_url).should have_been_made.once
389
+ end
390
+
391
+ it 'and @config[:eager_fetching] is false' do
392
+ override_config_key(:eager_fetching, false)
393
+ stub_deployment_req(config[:api_host], request_path(:some_key))
394
+ client.send(:fetch_data, :some_key)
395
+ a_request(:get, request_url(config[:api_host],
396
+ request_path(:some_key)))
397
+ .should have_been_made.once
398
+ end
399
+ end
400
+
401
+ it 'is returning response data' do
402
+ expect(client.send(:fetch_data).body).to eq(example_response)
403
+ end
404
+ end
405
+
406
+ context 'connection fail' do
407
+ context 'non 200 response status' do
408
+ before do
409
+ stub_request(:get, request_url)
410
+ .to_return(status: 400, body: 'fail', headers: {})
411
+ end
412
+
413
+ it 'tries REQUEST_TRY_COUNT times before giving-up' do
414
+ client.send(:fetch_data)
415
+ a_request(:get, request_url).should have_been_made
416
+ .times(Keystok::Client::REQUEST_TRY_COUNT)
417
+ end
418
+
419
+ it 'is returning non-200 response after 3 attempts' do
420
+ expect(client.send(:fetch_data).status).to eq(400)
421
+ end
422
+
423
+ it 'should log warning on every non-200 response' do
424
+ logger = double(:logger)
425
+ Keystok.logger = logger
426
+ expect(logger).to receive(:warn).exactly(3).times
427
+ .with('Keystok API response status: 400')
428
+ client.send(:fetch_data)
429
+ end
430
+ end
431
+
432
+ context 'Faraday exception' do
433
+ it 'tries REQUEST_TRY_COUNT times before giving-up' do
434
+ stub_request(:get, request_url)
435
+ .to_raise(Faraday::Error::ConnectionFailed)
436
+ begin
437
+ client.send(:fetch_data)
438
+ # rubocop:disable HandleExceptions
439
+ rescue Faraday::Error::ConnectionFailed
440
+ # rubocop:enable HandleExceptions
441
+ # Nothing here becouse we just want to check requests count
442
+ end
443
+ a_request(:get, request_url).should have_been_made
444
+ .times(Keystok::Client::REQUEST_TRY_COUNT)
445
+ end
446
+
447
+ it 'is re-raising Faraday exception after 3 attempts' do
448
+ stub_request(:get, request_url)
449
+ .to_raise(Faraday::Error::ConnectionFailed)
450
+ expect do
451
+ client.send(:fetch_data)
452
+ end.to raise_error(Faraday::Error::ConnectionFailed)
453
+ end
454
+
455
+ it 'should log warning on every Faraday exception' do
456
+ stub_request(:get, request_url)
457
+ .to_raise(Faraday::Error::ConnectionFailed)
458
+ logger = double(:logger)
459
+ Keystok.logger = logger
460
+ expect(logger).to receive(:warn).exactly(3).times
461
+ .with('Exception during Keystok API data fetch: ' \
462
+ 'Exception from WebMock')
463
+ begin
464
+ client.send(:fetch_data)
465
+ # rubocop:disable HandleExceptions
466
+ rescue Faraday::Error::ConnectionFailed
467
+ # rubocop:enable HandleExceptions
468
+ # Nothing here becouse we just want to check requests count
469
+ end
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ describe 'keys_to_symbols' do
476
+ it 'convert string hash keys to symbols' do
477
+ expect(client.send(:keys_to_symbols, 'key_1' => 1, 'key_2' => 2))
478
+ .to eq(key_1: 1, key_2: 2)
479
+ end
480
+
481
+ it 'leave non-string hash keys untouched' do
482
+ expect(client.send(:keys_to_symbols, :key_1 => 1, 2 => 2, [3] => 3))
483
+ .to eq(key_1: 1, 2 => 2, [3] => 3)
484
+ end
485
+ end
486
+
487
+ describe 'load_keys' do
488
+ before do
489
+ FileUtils.rm_f(tmp_filepath)
490
+ end
491
+
492
+ after do
493
+ FileUtils.rm_f(tmp_filepath)
494
+ end
495
+
496
+ context 'connection success' do
497
+ before do
498
+ stub_request(:get, request_url)
499
+ .to_return(status: 200, body: example_response, headers: {})
500
+ end
501
+
502
+ it 'is triggering @keys_store setting' do
503
+ client.send(:load_keys)
504
+ expect(client.instance_variable_get('@keys_store'))
505
+ .to eq('key_1' => 'Value 1', 'key_2' => 'Value 2')
506
+ end
507
+
508
+ it 'is returning loaded keys' do
509
+ expect(client.send(:load_keys)).to eq('key_1' => 'Value 1',
510
+ 'key_2' => 'Value 2')
511
+ end
512
+
513
+ it 'is writing data to cache' do
514
+ expect(File.size?(tmp_filepath).to_i).to eq(0)
515
+ client.send(:load_keys)
516
+ expect(File.size?(tmp_filepath).to_i).to_not eq(0)
517
+ end
518
+
519
+ it 'is not writing data to cache when no_cache is set in config' do
520
+ override_config_key(:no_cache, true)
521
+ expect(File.size?(tmp_filepath).to_i).to eq(0)
522
+ client.send(:load_keys)
523
+ expect(File.size?(tmp_filepath).to_i).to eq(0)
524
+ end
525
+
526
+ it 'is discarding old keys' do
527
+ client.instance_variable_set('@keys_store', 'old' => 'value')
528
+ expect(client.send(:load_keys)).to eq('key_1' => 'Value 1',
529
+ 'key_2' => 'Value 2')
530
+ end
531
+
532
+ it "when called with key name it passes key to 'fetch' method" do
533
+ stub_deployment_req(config[:api_host], request_path(:some_key))
534
+ override_config_key(:eager_fetching, false)
535
+ expect(client).to receive(:fetch_data).with(:some_key)
536
+ .and_call_original
537
+ client.send(:load_keys, :some_key)
538
+ end
539
+ end
540
+
541
+ context 'connection fail' do
542
+ it 'is raising ConnectionError on no cache and response != 200' do
543
+ stub_request(:get, request_url)
544
+ .to_return(status: 400, body: 'fail', headers: {})
545
+ expect do
546
+ client.send(:load_keys)
547
+ end.to raise_error(Keystok::Error::ConnectionError,
548
+ "No cache data and response code:\n" \
549
+ '400 with body: fail')
550
+ end
551
+
552
+ it 'is raising ConnectionError on no cache and Faraday exception' do
553
+ stub_request(:get, request_url)
554
+ .to_raise(Faraday::Error::ConnectionFailed)
555
+ expect do
556
+ client.send(:load_keys)
557
+ end.to raise_error(Keystok::Error::ConnectionError,
558
+ /No cache data and connection error:.*/)
559
+ end
560
+ end
561
+
562
+ context 'load from cache' do
563
+ it 'loads cached data on response != 200' do
564
+ stub_request(:get, request_url)
565
+ .to_return(status: 200, body: example_response, headers: {})
566
+ client.send(:load_keys)
567
+ stub_request(:get, request_url)
568
+ .to_return(status: 400, body: 'fail', headers: {})
569
+ expect(client.send(:load_keys)).to eq('key_1' => 'Value 1',
570
+ 'key_2' => 'Value 2')
571
+ end
572
+
573
+ it 'loads cached data on Faraday exception' do
574
+ stub_request(:get, request_url)
575
+ .to_return(status: 200, body: example_response, headers: {})
576
+ client.send(:load_keys)
577
+ stub_request(:get, request_url)
578
+ .to_raise(Faraday::Error::ConnectionFailed)
579
+ expect(client.send(:load_keys)).to eq('key_1' => 'Value 1',
580
+ 'key_2' => 'Value 2')
581
+ end
582
+ end
583
+ end
584
+
585
+ describe 'oauth_client' do
586
+ it 'initialize oauth client' do
587
+ oauth_client = client.send(:oauth_client)
588
+ expect(oauth_client.id).to eq(nil)
589
+ expect(oauth_client.secret).to eq(nil)
590
+ expect(oauth_client.site).to eq(config[:auth_host])
591
+ end
592
+
593
+ it "is using 'https://keystok.com' when auth_host is nil" do
594
+ override_config_key(:auth_host)
595
+ oauth_client = client.send(:oauth_client)
596
+ expect(oauth_client.site).to eq('https://keystok.com')
597
+ end
598
+
599
+ it 'caches the instance' do
600
+ oauth_client_1 = client.send(:oauth_client)
601
+ oauth_client_2 = client.send(:oauth_client)
602
+ expect(oauth_client_2).to equal(oauth_client_1)
603
+ end
604
+ end
605
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under GitDock Oy license terms.
3
+ # See https://bitbucket.org/keystok/keystok-client-ruby/raw/master/LICENSE.txt
4
+ # for details.
5
+
6
+ require 'spec_helper'
7
+
8
+ describe Keystok do
9
+ describe 'logger' do
10
+ before do
11
+ @rails = Rails
12
+ Kernel.silence_warnings do
13
+ # rubocop: disable ConstantName
14
+ Rails = nil
15
+ # rubocop: enable ConstantName
16
+ end
17
+ load 'keystok.rb'
18
+ end
19
+
20
+ after do
21
+ Kernel.silence_warnings do
22
+ # rubocop: disable ConstantName
23
+ Rails = @rails
24
+ # rubocop: enable ConstantName
25
+ end
26
+ load 'keystok.rb'
27
+ end
28
+
29
+ it 'create default logger' do
30
+ expect(Keystok.logger.respond_to?(:warn)).to eq(true)
31
+ end
32
+ end
33
+ end