prefab-cloud-ruby 0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/workflows/ruby.yml +46 -0
  4. data/.gitmodules +3 -0
  5. data/.rubocop.yml +13 -0
  6. data/.tool-versions +1 -0
  7. data/CHANGELOG.md +169 -0
  8. data/CODEOWNERS +1 -0
  9. data/Gemfile +26 -0
  10. data/Gemfile.lock +188 -0
  11. data/LICENSE.txt +20 -0
  12. data/README.md +94 -0
  13. data/Rakefile +50 -0
  14. data/VERSION +1 -0
  15. data/bin/console +21 -0
  16. data/compile_protos.sh +18 -0
  17. data/lib/prefab/client.rb +153 -0
  18. data/lib/prefab/config_client.rb +292 -0
  19. data/lib/prefab/config_client_presenter.rb +18 -0
  20. data/lib/prefab/config_loader.rb +84 -0
  21. data/lib/prefab/config_resolver.rb +77 -0
  22. data/lib/prefab/config_value_unwrapper.rb +115 -0
  23. data/lib/prefab/config_value_wrapper.rb +18 -0
  24. data/lib/prefab/context.rb +179 -0
  25. data/lib/prefab/context_shape.rb +20 -0
  26. data/lib/prefab/context_shape_aggregator.rb +65 -0
  27. data/lib/prefab/criteria_evaluator.rb +136 -0
  28. data/lib/prefab/encryption.rb +65 -0
  29. data/lib/prefab/error.rb +6 -0
  30. data/lib/prefab/errors/env_var_parse_error.rb +11 -0
  31. data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
  32. data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
  33. data/lib/prefab/errors/missing_default_error.rb +13 -0
  34. data/lib/prefab/errors/missing_env_var_error.rb +11 -0
  35. data/lib/prefab/errors/uninitialized_error.rb +13 -0
  36. data/lib/prefab/evaluation.rb +52 -0
  37. data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
  38. data/lib/prefab/example_contexts_aggregator.rb +78 -0
  39. data/lib/prefab/exponential_backoff.rb +21 -0
  40. data/lib/prefab/feature_flag_client.rb +42 -0
  41. data/lib/prefab/http_connection.rb +41 -0
  42. data/lib/prefab/internal_logger.rb +16 -0
  43. data/lib/prefab/local_config_parser.rb +151 -0
  44. data/lib/prefab/log_path_aggregator.rb +69 -0
  45. data/lib/prefab/logger_client.rb +264 -0
  46. data/lib/prefab/murmer3.rb +50 -0
  47. data/lib/prefab/options.rb +208 -0
  48. data/lib/prefab/periodic_sync.rb +69 -0
  49. data/lib/prefab/prefab.rb +56 -0
  50. data/lib/prefab/rate_limit_cache.rb +41 -0
  51. data/lib/prefab/resolved_config_presenter.rb +86 -0
  52. data/lib/prefab/time_helpers.rb +7 -0
  53. data/lib/prefab/weighted_value_resolver.rb +42 -0
  54. data/lib/prefab/yaml_config_parser.rb +34 -0
  55. data/lib/prefab-cloud-ruby.rb +57 -0
  56. data/lib/prefab_pb.rb +93 -0
  57. data/prefab-cloud-ruby.gemspec +155 -0
  58. data/test/.prefab.default.config.yaml +2 -0
  59. data/test/.prefab.unit_tests.config.yaml +28 -0
  60. data/test/integration_test.rb +150 -0
  61. data/test/integration_test_helpers.rb +151 -0
  62. data/test/support/common_helpers.rb +180 -0
  63. data/test/support/mock_base_client.rb +42 -0
  64. data/test/support/mock_config_client.rb +19 -0
  65. data/test/support/mock_config_loader.rb +1 -0
  66. data/test/test_client.rb +444 -0
  67. data/test/test_config_client.rb +109 -0
  68. data/test/test_config_loader.rb +117 -0
  69. data/test/test_config_resolver.rb +430 -0
  70. data/test/test_config_value_unwrapper.rb +224 -0
  71. data/test/test_config_value_wrapper.rb +42 -0
  72. data/test/test_context.rb +203 -0
  73. data/test/test_context_shape.rb +50 -0
  74. data/test/test_context_shape_aggregator.rb +147 -0
  75. data/test/test_criteria_evaluator.rb +726 -0
  76. data/test/test_encryption.rb +16 -0
  77. data/test/test_evaluation_summary_aggregator.rb +162 -0
  78. data/test/test_example_contexts_aggregator.rb +238 -0
  79. data/test/test_exponential_backoff.rb +18 -0
  80. data/test/test_feature_flag_client.rb +48 -0
  81. data/test/test_helper.rb +17 -0
  82. data/test/test_integration.rb +58 -0
  83. data/test/test_local_config_parser.rb +147 -0
  84. data/test/test_log_path_aggregator.rb +62 -0
  85. data/test/test_logger.rb +621 -0
  86. data/test/test_logger_initialization.rb +12 -0
  87. data/test/test_options.rb +75 -0
  88. data/test/test_prefab.rb +12 -0
  89. data/test/test_rate_limit_cache.rb +44 -0
  90. data/test/test_weighted_value_resolver.rb +71 -0
  91. metadata +337 -0
@@ -0,0 +1,444 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestClient < Minitest::Test
6
+ LOCAL_ONLY = Prefab::Options::DATASOURCES::LOCAL_ONLY
7
+
8
+ PROJECT_ENV_ID = 1
9
+ KEY = 'the-key'
10
+ DEFAULT_VALUE = 'default_value'
11
+ DESIRED_VALUE = 'desired_value'
12
+
13
+ IRRELEVANT = 'this should never show up'
14
+
15
+ DEFAULT_VALUE_CONFIG = PrefabProto::ConfigValue.new(string: DEFAULT_VALUE)
16
+ DESIRED_VALUE_CONFIG = PrefabProto::ConfigValue.new(string: DESIRED_VALUE)
17
+
18
+ TRUE_CONFIG = PrefabProto::ConfigValue.new(bool: true)
19
+ FALSE_CONFIG = PrefabProto::ConfigValue.new(bool: false)
20
+
21
+ DEFAULT_ROW = PrefabProto::ConfigRow.new(
22
+ values: [
23
+ PrefabProto::ConditionalValue.new(value: DEFAULT_VALUE_CONFIG)
24
+ ]
25
+ )
26
+
27
+ def test_get
28
+ _, err = capture_io do
29
+ client = new_client
30
+ assert_equal 'default', client.get('does.not.exist', 'default')
31
+ assert_equal 'test sample value', client.get('sample')
32
+ assert_equal 123, client.get('sample_int')
33
+ end
34
+ assert_equal '', err
35
+ end
36
+
37
+ def test_get_with_default
38
+ client = new_client
39
+ # A `false` value is not replaced with the default
40
+ assert_equal false, client.get('false_value', 'red')
41
+
42
+ # A falsy value is not replaced with the default
43
+ assert_equal 0, client.get('zero_value', 'red')
44
+
45
+ # A missing value returns the default
46
+ assert_equal 'buckets', client.get('missing_value', 'buckets')
47
+ end
48
+
49
+ def test_get_with_missing_default
50
+ client = new_client
51
+ # it raises by default
52
+ err = assert_raises(Prefab::Errors::MissingDefaultError) do
53
+ assert_nil client.get('missing_value')
54
+ end
55
+
56
+ assert_match(/No value found for key/, err.message)
57
+ assert_match(/on_no_default/, err.message)
58
+
59
+ # you can opt-in to return `nil` instead
60
+ client = new_client(on_no_default: Prefab::Options::ON_NO_DEFAULT::RETURN_NIL)
61
+ assert_nil client.get('missing_value')
62
+ end
63
+
64
+ def test_enabled
65
+ client = new_client
66
+ assert_equal false, client.enabled?('does_not_exist')
67
+ assert_equal true, client.enabled?('enabled_flag')
68
+ assert_equal false, client.enabled?('disabled_flag')
69
+ assert_equal false, client.enabled?('flag_with_a_value')
70
+ end
71
+
72
+ def test_ff_enabled_with_user_key_match
73
+ client = new_client
74
+
75
+ ctx = { user: { key: 'jimmy' } }
76
+ assert_equal false, client.enabled?('user_key_match', ctx)
77
+ assert_equal false, Prefab::Context.with_context(ctx) { client.enabled?('user_key_match') }
78
+
79
+ ctx = { user: { key: 'abc123' } }
80
+ assert_equal true, client.enabled?('user_key_match', ctx)
81
+ assert_equal true, Prefab::Context.with_context(ctx) { client.enabled?('user_key_match') }
82
+
83
+ ctx = { user: { key: 'xyz987' } }
84
+ assert_equal true, client.enabled?('user_key_match', ctx)
85
+ assert_equal true, Prefab::Context.with_context(ctx) { client.enabled?('user_key_match') }
86
+ end
87
+
88
+ # NOTE: these are all `false` because we're doing a enabled? on a FF with string variants
89
+ # see test_ff_get_with_context for the raw value tests
90
+ def test_ff_enabled_with_context
91
+ client = new_client
92
+
93
+ ctx = { user: { domain: 'gmail.com' } }
94
+ assert_equal false, client.enabled?('just_my_domain', ctx)
95
+ assert_equal false, Prefab::Context.with_context(ctx) { client.enabled?('just_my_domain') }
96
+
97
+ ctx = { user: { domain: 'prefab.cloud' } }
98
+ assert_equal false, client.enabled?('just_my_domain', ctx)
99
+ assert_equal false, Prefab::Context.with_context(ctx) { client.enabled?('just_my_domain') }
100
+
101
+ ctx = { user: { domain: 'example.com' } }
102
+ assert_equal false, client.enabled?('just_my_domain', ctx)
103
+ assert_equal false, Prefab::Context.with_context(ctx) { client.enabled?('just_my_domain') }
104
+ end
105
+
106
+ def test_ff_get_with_context
107
+ client = new_client
108
+
109
+ ctx = { user: { domain: 'gmail.com' } }
110
+ assert_equal 'DEFAULT', client.get('just_my_domain', 'DEFAULT', ctx)
111
+ assert_equal 'DEFAULT', Prefab::Context.with_context(ctx) { client.get('just_my_domain', 'DEFAULT') }
112
+
113
+ ctx = { user: { domain: 'prefab.cloud' } }
114
+ assert_equal 'new-version', client.get('just_my_domain', 'DEFAULT', ctx)
115
+ assert_equal 'new-version', Prefab::Context.with_context(ctx) { client.get('just_my_domain', 'DEFAULT') }
116
+
117
+ ctx = { user: { domain: 'example.com' } }
118
+ assert_equal 'new-version', client.get('just_my_domain', 'DEFAULT', ctx)
119
+ assert_equal 'new-version', Prefab::Context.with_context(ctx) { client.get('just_my_domain', 'DEFAULT') }
120
+ end
121
+
122
+ def test_deprecated_no_dot_notation_ff_enabled_with_jit_context
123
+ client = new_client
124
+ # with no lookup key
125
+ assert_equal false, client.enabled?('deprecated_no_dot_notation', { domain: 'gmail.com' })
126
+ assert_equal true, client.enabled?('deprecated_no_dot_notation', { domain: 'prefab.cloud' })
127
+ assert_equal true, client.enabled?('deprecated_no_dot_notation', { domain: 'example.com' })
128
+
129
+ assert_stderr [
130
+ "[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash.",
131
+ "[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash.",
132
+ "[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash."
133
+ ]
134
+ end
135
+
136
+ def test_getting_feature_flag_value
137
+ client = new_client
138
+ assert_equal false, client.enabled?('flag_with_a_value')
139
+ assert_equal 'all-features', client.get('flag_with_a_value')
140
+ end
141
+
142
+ def test_initialization_with_an_options_object
143
+ options_hash = {
144
+ namespace: 'test-namespace',
145
+ prefab_datasources: LOCAL_ONLY
146
+ }
147
+
148
+ options = Prefab::Options.new(options_hash)
149
+
150
+ client = Prefab::Client.new(options)
151
+
152
+ assert_equal client.namespace, 'test-namespace'
153
+ end
154
+
155
+ def test_initialization_with_a_hash
156
+ options_hash = {
157
+ namespace: 'test-namespace',
158
+ prefab_datasources: LOCAL_ONLY
159
+ }
160
+
161
+ client = Prefab::Client.new(options_hash)
162
+
163
+ assert_equal client.namespace, 'test-namespace'
164
+ end
165
+
166
+ def test_evaluation_summary_aggregator
167
+ fake_api_key = '123-development-yourapikey-SDK'
168
+
169
+ # it is nil by default
170
+ assert_nil new_client(api_key: fake_api_key).evaluation_summary_aggregator
171
+
172
+ # it is nil when local_only even if collect_max_evaluation_summaries is true
173
+ assert_nil new_client(prefab_datasources: LOCAL_ONLY,
174
+ collect_evaluation_summaries: true, ).evaluation_summary_aggregator
175
+
176
+ # it is nil when collect_max_evaluation_summaries is false
177
+ assert_nil new_client(api_key: fake_api_key,
178
+ prefab_datasources: :all,
179
+ collect_evaluation_summaries: false).evaluation_summary_aggregator
180
+
181
+ # it is not nil when collect_max_evaluation_summaries is true and the datasource is not local_only
182
+ assert_equal Prefab::EvaluationSummaryAggregator,
183
+ new_client(api_key: fake_api_key,
184
+ prefab_datasources: :all,
185
+ collect_evaluation_summaries: true).evaluation_summary_aggregator.class
186
+
187
+ assert_logged [
188
+ "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient No success loading checkpoints"
189
+ ]
190
+ end
191
+
192
+ def test_get_with_basic_value
193
+ config = basic_value_config
194
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID, collect_evaluation_summaries: true,
195
+ context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
196
+
197
+ assert_equal DESIRED_VALUE, client.get(config.key, IRRELEVANT, 'user' => { 'key' => 99 })
198
+
199
+ assert_summary client, {
200
+ [KEY, :CONFIG] => {
201
+ {
202
+ config_id: config.id,
203
+ config_row_index: 1,
204
+ selected_value: DESIRED_VALUE_CONFIG,
205
+ conditional_value_index: 0,
206
+ weighted_value_index: nil,
207
+ selected_index: nil
208
+ } => 1
209
+ }
210
+ }
211
+
212
+ assert_example_contexts client, [Prefab::Context.new({ user: { 'key' => 99 } })]
213
+ end
214
+
215
+ def test_get_with_basic_value_with_context
216
+ config = basic_value_config
217
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID, collect_evaluation_summaries: true,
218
+ context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
219
+
220
+ client.with_context('user' => { 'key' => 99 }) do
221
+ assert_equal DESIRED_VALUE, client.get(config.key)
222
+ end
223
+
224
+ assert_summary client, {
225
+ [KEY, :CONFIG] => {
226
+ {
227
+ config_id: config.id,
228
+ config_row_index: 1,
229
+ selected_value: DESIRED_VALUE_CONFIG,
230
+ conditional_value_index: 0,
231
+ weighted_value_index: nil,
232
+ selected_index: nil
233
+ } => 1
234
+ }
235
+ }
236
+
237
+ assert_example_contexts client, [Prefab::Context.new({ user: { 'key' => 99 } })]
238
+ end
239
+
240
+ def test_get_with_weighted_values
241
+ config = PrefabProto::Config.new(
242
+ id: 123,
243
+ key: KEY,
244
+ config_type: PrefabProto::ConfigType::CONFIG,
245
+ rows: [
246
+ DEFAULT_ROW,
247
+ PrefabProto::ConfigRow.new(
248
+ project_env_id: PROJECT_ENV_ID,
249
+ values: [
250
+ PrefabProto::ConditionalValue.new(
251
+ criteria: [PrefabProto::Criterion.new(operator: PrefabProto::Criterion::CriterionOperator::ALWAYS_TRUE)],
252
+ value: PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 98], ['def', 1],
253
+ ['ghi', 1]]))
254
+ )
255
+ ]
256
+ )
257
+ ]
258
+ )
259
+
260
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID, collect_evaluation_summaries: true,
261
+ context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
262
+
263
+ 2.times do
264
+ assert_equal 'abc', client.get(config.key, IRRELEVANT, 'user' => { 'key' => '1' })
265
+ end
266
+
267
+ 3.times do
268
+ assert_equal 'def', client.get(config.key, IRRELEVANT, 'user' => { 'key' => '12' })
269
+ end
270
+
271
+ assert_equal 'ghi',
272
+ client.get(config.key, IRRELEVANT, 'user' => { 'key' => '4', admin: true })
273
+
274
+ assert_summary client, {
275
+ [KEY, :CONFIG] => {
276
+ {
277
+ config_id: config.id,
278
+ config_row_index: 1,
279
+ selected_value: PrefabProto::ConfigValue.new(string: 'abc'),
280
+ conditional_value_index: 0,
281
+ weighted_value_index: 0,
282
+ selected_index: nil
283
+ } => 2,
284
+
285
+ {
286
+ config_id: config.id,
287
+ config_row_index: 1,
288
+ selected_value: PrefabProto::ConfigValue.new(string: 'def'),
289
+ conditional_value_index: 0,
290
+ weighted_value_index: 1,
291
+ selected_index: nil
292
+ } => 3,
293
+
294
+ {
295
+ config_id: config.id,
296
+ config_row_index: 1,
297
+ selected_value: PrefabProto::ConfigValue.new(string: 'ghi'),
298
+ conditional_value_index: 0,
299
+ weighted_value_index: 2,
300
+ selected_index: nil
301
+ } => 1
302
+ }
303
+ }
304
+
305
+ assert_example_contexts client, [
306
+ Prefab::Context.new(user: { 'key' => '1' }),
307
+ Prefab::Context.new(user: { 'key' => '12' }),
308
+ Prefab::Context.new(user: { 'key' => '4', admin: true })
309
+ ]
310
+ end
311
+
312
+ def test_in_seg
313
+ segment_key = 'segment_key'
314
+
315
+ segment_config = PrefabProto::Config.new(
316
+ config_type: PrefabProto::ConfigType::SEGMENT,
317
+ key: segment_key,
318
+ rows: [
319
+ PrefabProto::ConfigRow.new(
320
+ values: [
321
+ PrefabProto::ConditionalValue.new(
322
+ value: TRUE_CONFIG,
323
+ criteria: [
324
+ PrefabProto::Criterion.new(
325
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
326
+ value_to_match: string_list(['hotmail.com', 'gmail.com']),
327
+ property_name: 'user.email'
328
+ )
329
+ ]
330
+ ),
331
+ PrefabProto::ConditionalValue.new(value: FALSE_CONFIG)
332
+ ]
333
+ )
334
+ ]
335
+ )
336
+
337
+ config = PrefabProto::Config.new(
338
+ key: KEY,
339
+ rows: [
340
+ DEFAULT_ROW,
341
+
342
+ PrefabProto::ConfigRow.new(
343
+ project_env_id: PROJECT_ENV_ID,
344
+ values: [
345
+ PrefabProto::ConditionalValue.new(
346
+ criteria: [
347
+ PrefabProto::Criterion.new(
348
+ operator: PrefabProto::Criterion::CriterionOperator::IN_SEG,
349
+ value_to_match: PrefabProto::ConfigValue.new(string: segment_key)
350
+ )
351
+ ],
352
+ value: DESIRED_VALUE_CONFIG
353
+ )
354
+ ]
355
+ )
356
+ ]
357
+ )
358
+
359
+ client = new_client(config: [config, segment_config], project_env_id: PROJECT_ENV_ID,
360
+ collect_evaluation_summaries: true, context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
361
+
362
+ assert_equal DEFAULT_VALUE, client.get(config.key)
363
+ assert_equal DEFAULT_VALUE,
364
+ client.get(config.key, IRRELEVANT, user: { key: 'abc', email: 'example@prefab.cloud' })
365
+ assert_equal DESIRED_VALUE, client.get(config.key, IRRELEVANT, user: { key: 'def', email: 'example@hotmail.com' })
366
+
367
+ assert_summary client, {
368
+ [segment_key, :SEGMENT] => {
369
+ { config_id: 0, config_row_index: 0, conditional_value_index: 1, selected_value: FALSE_CONFIG,
370
+ weighted_value_index: nil, selected_index: nil } => 2,
371
+ { config_id: 0, config_row_index: 0, conditional_value_index: 0, selected_value: TRUE_CONFIG,
372
+ weighted_value_index: nil, selected_index: nil } => 1
373
+ },
374
+ [KEY, :NOT_SET_CONFIG_TYPE] => {
375
+ { config_id: 0, config_row_index: 0, conditional_value_index: 0, selected_value: DEFAULT_VALUE_CONFIG,
376
+ weighted_value_index: nil, selected_index: nil } => 2,
377
+ { config_id: 0, config_row_index: 1, conditional_value_index: 0, selected_value: DESIRED_VALUE_CONFIG,
378
+ weighted_value_index: nil, selected_index: nil } => 1
379
+ }
380
+ }
381
+
382
+ assert_example_contexts client, [
383
+ Prefab::Context.new(user: { key: 'abc', email: 'example@prefab.cloud' }),
384
+ Prefab::Context.new(user: { key: 'def', email: 'example@hotmail.com' })
385
+ ]
386
+ end
387
+
388
+ def test_get_log_level
389
+ config = PrefabProto::Config.new(
390
+ id: 999,
391
+ key: 'log-level',
392
+ config_type: PrefabProto::ConfigType::LOG_LEVEL,
393
+ rows: [
394
+ PrefabProto::ConfigRow.new(
395
+ values: [
396
+ PrefabProto::ConditionalValue.new(
397
+ criteria: [PrefabProto::Criterion.new(operator: PrefabProto::Criterion::CriterionOperator::ALWAYS_TRUE)],
398
+ value: PrefabProto::ConfigValue.new(log_level: PrefabProto::LogLevel::INFO)
399
+ )
400
+ ]
401
+ )
402
+ ]
403
+ )
404
+
405
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID,
406
+ collect_evaluation_summaries: true, allow_telemetry_in_local_mode: true)
407
+
408
+ assert_equal :INFO, client.get(config.key, IRRELEVANT)
409
+
410
+ # nothing is summarized for log levels
411
+ assert_summary client, {}
412
+ end
413
+
414
+ def test_fork_includes_logger_context_keys
415
+ client = new_client
416
+ client.log.add_context_keys "user.name"
417
+
418
+ forked = client.fork
419
+
420
+ assert forked.log.context_keys.to_a == %w(user.name)
421
+ end
422
+
423
+ private
424
+
425
+ def basic_value_config
426
+ PrefabProto::Config.new(
427
+ id: 123,
428
+ key: KEY,
429
+ config_type: PrefabProto::ConfigType::CONFIG,
430
+ rows: [
431
+ DEFAULT_ROW,
432
+ PrefabProto::ConfigRow.new(
433
+ project_env_id: PROJECT_ENV_ID,
434
+ values: [
435
+ PrefabProto::ConditionalValue.new(
436
+ criteria: [PrefabProto::Criterion.new(operator: PrefabProto::Criterion::CriterionOperator::ALWAYS_TRUE)],
437
+ value: DESIRED_VALUE_CONFIG
438
+ )
439
+ ]
440
+ )
441
+ ]
442
+ )
443
+ end
444
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigClient < Minitest::Test
6
+ def setup
7
+ super
8
+ options = Prefab::Options.new(
9
+ prefab_config_override_dir: 'none',
10
+ prefab_config_classpath_dir: 'test',
11
+ prefab_envs: 'unit_tests',
12
+ prefab_datasources: Prefab::Options::DATASOURCES::LOCAL_ONLY,
13
+ x_use_local_cache: true,
14
+ )
15
+
16
+ @config_client = Prefab::ConfigClient.new(MockBaseClient.new(options), 10)
17
+ end
18
+
19
+ def test_load
20
+ assert_equal 'test sample value', @config_client.get('sample')
21
+ assert_equal 123, @config_client.get('sample_int')
22
+ assert_equal 12.12, @config_client.get('sample_double')
23
+ assert_equal true, @config_client.get('sample_bool')
24
+ assert_equal :ERROR, @config_client.get('log-level.app')
25
+ end
26
+
27
+ def test_initialization_timeout_error
28
+ options = Prefab::Options.new(
29
+ api_key: '123-ENV-KEY-SDK',
30
+ initialization_timeout_sec: 0.01,
31
+ logdev: StringIO.new
32
+ )
33
+
34
+ err = assert_raises(Prefab::Errors::InitializationTimeoutError) do
35
+ Prefab::Client.new(options).config_client.get('anything')
36
+ end
37
+
38
+ assert_match(/couldn't initialize in 0.01 second timeout/, err.message)
39
+ end
40
+
41
+ def test_prefab_envs_is_forgiving
42
+ assert_equal ['my_env'], Prefab::Options.new(
43
+ prefab_envs: 'my_env'
44
+ ).prefab_envs
45
+
46
+ assert_equal %w[my_env a_second_env], Prefab::Options.new(
47
+ prefab_envs: %w[my_env a_second_env]
48
+ ).prefab_envs
49
+ end
50
+
51
+ def test_prefab_envs_env_var
52
+ ENV['PREFAB_ENVS'] = 'one,two'
53
+ assert_equal %w[one two], Prefab::Options.new.prefab_envs
54
+ end
55
+
56
+ def test_invalid_api_key_error
57
+ options = Prefab::Options.new(
58
+ api_key: ''
59
+ )
60
+
61
+ err = assert_raises(Prefab::Errors::InvalidApiKeyError) do
62
+ Prefab::Client.new(options).config_client.get('anything')
63
+ end
64
+
65
+ assert_match(/No API key/, err.message)
66
+
67
+ options = Prefab::Options.new(
68
+ api_key: 'invalid'
69
+ )
70
+
71
+ err = assert_raises(Prefab::Errors::InvalidApiKeyError) do
72
+ Prefab::Client.new(options).config_client.get('anything')
73
+ end
74
+
75
+ assert_match(/format is invalid/, err.message)
76
+ end
77
+
78
+ def test_caching
79
+ @config_client.send(:cache_configs,
80
+ PrefabProto::Configs.new(configs:
81
+ [PrefabProto::Config.new(key: 'test', id: 1,
82
+ rows: [PrefabProto::ConfigRow.new(
83
+ values: [
84
+ PrefabProto::ConditionalValue.new(
85
+ value: PrefabProto::ConfigValue.new(string: "test value")
86
+ )
87
+ ]
88
+ )])],
89
+ config_service_pointer: PrefabProto::ConfigServicePointer.new(project_id: 3, project_env_id: 5)))
90
+ @config_client.send(:load_cache)
91
+ assert_equal "test value", @config_client.get("test")
92
+ end
93
+
94
+ def test_cache_path_respects_xdg
95
+ options = Prefab::Options.new(
96
+ prefab_datasources: Prefab::Options::DATASOURCES::LOCAL_ONLY,
97
+ x_use_local_cache: true,
98
+ api_key: "123-ENV-KEY-SDK",)
99
+
100
+ config_client = Prefab::ConfigClient.new(MockBaseClient.new(options), 10)
101
+ assert_equal "#{Dir.home}/.cache/prefab.cache.123.json", config_client.send(:cache_path)
102
+
103
+ with_env('XDG_CACHE_HOME', '/tmp') do
104
+ config_client = Prefab::ConfigClient.new(MockBaseClient.new(options), 10)
105
+ assert_equal "/tmp/prefab.cache.123.json", config_client.send(:cache_path)
106
+ end
107
+ end
108
+
109
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigLoader < Minitest::Test
6
+ def setup
7
+ super
8
+ options = Prefab::Options.new(
9
+ prefab_config_override_dir: 'none',
10
+ prefab_config_classpath_dir: 'test',
11
+ prefab_envs: 'unit_tests'
12
+ )
13
+ @loader = Prefab::ConfigLoader.new(MockBaseClient.new(options))
14
+ end
15
+
16
+ def test_load
17
+ should_be :int, 123, 'sample_int'
18
+ should_be :string, 'test sample value', 'sample'
19
+ should_be :bool, true, 'sample_bool'
20
+ should_be :double, 12.12, 'sample_double'
21
+ end
22
+
23
+ def test_nested
24
+ should_be :string, 'nested value', 'nested.values.string'
25
+ should_be :string, 'top level', 'nested.values'
26
+ should_be :log_level, :ERROR, 'log-level.app'
27
+ should_be :log_level, :WARN, 'log-level.app.controller.hello'
28
+ should_be :log_level, :INFO, 'log-level.app.controller.hello.index'
29
+ end
30
+
31
+ def test_invalid_log_level
32
+ should_be :log_level, :NOT_SET_LOG_LEVEL, 'log-level.invalid'
33
+ end
34
+
35
+ def test_load_without_unit_test_env
36
+ options = Prefab::Options.new(
37
+ prefab_config_override_dir: 'none',
38
+ prefab_config_classpath_dir: 'test'
39
+ # no prefab_envs
40
+ )
41
+ @loader = Prefab::ConfigLoader.new(MockBaseClient.new(options))
42
+ should_be :string, 'default sample value', 'sample'
43
+ should_be :bool, true, 'sample_bool'
44
+ end
45
+
46
+ def test_highwater
47
+ assert_equal 0, @loader.highwater_mark
48
+ @loader.set(PrefabProto::Config.new(id: 1, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 456))]),
49
+ 'test')
50
+ assert_equal 1, @loader.highwater_mark
51
+
52
+ @loader.set(PrefabProto::Config.new(id: 5, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 456))]),
53
+ 'test')
54
+ assert_equal 5, @loader.highwater_mark
55
+ @loader.set(PrefabProto::Config.new(id: 2, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 456))]),
56
+ 'test')
57
+ assert_equal 5, @loader.highwater_mark
58
+ end
59
+
60
+ def test_keeps_most_recent
61
+ assert_equal 0, @loader.highwater_mark
62
+ @loader.set(PrefabProto::Config.new(id: 1, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 1))]),
63
+ 'test')
64
+ assert_equal 1, @loader.highwater_mark
65
+ should_be :int, 1, 'sample_int'
66
+
67
+ @loader.set(PrefabProto::Config.new(id: 4, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 4))]),
68
+ 'test')
69
+ assert_equal 4, @loader.highwater_mark
70
+ should_be :int, 4, 'sample_int'
71
+
72
+ @loader.set(PrefabProto::Config.new(id: 2, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 2))]),
73
+ 'test')
74
+ assert_equal 4, @loader.highwater_mark
75
+ should_be :int, 4, 'sample_int'
76
+ end
77
+
78
+ def test_api_precedence
79
+ should_be :int, 123, 'sample_int'
80
+
81
+ @loader.set(PrefabProto::Config.new(key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 456))]), 'test')
82
+ should_be :int, 456, 'sample_int'
83
+ end
84
+
85
+ def test_api_deltas
86
+ val = PrefabProto::ConfigValue.new(int: 456)
87
+ config = PrefabProto::Config.new(key: 'sample_int', rows: [config_row(val)])
88
+ @loader.set(config, 'test')
89
+
90
+ configs = PrefabProto::Configs.new
91
+ configs.configs << config
92
+ assert_equal configs, @loader.get_api_deltas
93
+ end
94
+
95
+ def test_loading_tombstones_removes_entries
96
+ val = PrefabProto::ConfigValue.new(int: 456)
97
+ config = PrefabProto::Config.new(key: 'sample_int', rows: [config_row(val)], id: 2)
98
+ @loader.set(config, 'test')
99
+
100
+ config = PrefabProto::Config.new(key: 'sample_int', rows: [], id: 3)
101
+ @loader.set(config, 'test')
102
+
103
+ configs = PrefabProto::Configs.new
104
+ assert_equal configs, @loader.get_api_deltas
105
+ end
106
+
107
+ private
108
+
109
+ def should_be(type, value, key)
110
+ assert_equal type, @loader.calc_config[key][:config].rows[0].values[0].value.type
111
+ assert_equal value, @loader.calc_config[key][:config].rows[0].values[0].value.send(type)
112
+ end
113
+
114
+ def config_row(value)
115
+ PrefabProto::ConfigRow.new(values: [PrefabProto::ConditionalValue.new(value: value)])
116
+ end
117
+ end