keystok 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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