prefab-cloud-ruby 0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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