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,430 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigResolver < Minitest::Test
6
+ STAGING_ENV_ID = 1
7
+ PRODUCTION_ENV_ID = 2
8
+ TEST_ENV_ID = 3
9
+ SEGMENT_KEY = 'segment_key'
10
+ CONFIG_KEY = 'config_key'
11
+ DEFAULT_VALUE = 'default_value'
12
+ DESIRED_VALUE = 'desired_value'
13
+ IN_SEGMENT_VALUE = 'in_segment_value'
14
+ WRONG_ENV_VALUE = 'wrong_env_value'
15
+ NOT_IN_SEGMENT_VALUE = 'not_in_segment_value'
16
+
17
+ DEFAULT_ROW = PrefabProto::ConfigRow.new(
18
+ values: [
19
+ PrefabProto::ConditionalValue.new(
20
+ value: PrefabProto::ConfigValue.new(string: DEFAULT_VALUE)
21
+ )
22
+ ]
23
+ )
24
+
25
+ class MockConfigLoader
26
+ def calc_config; end
27
+ end
28
+
29
+ def test_resolution
30
+ @loader = MockConfigLoader.new
31
+
32
+ loaded_values = {
33
+ 'key' => { config: PrefabProto::Config.new(
34
+ key: 'key',
35
+ rows: [
36
+ DEFAULT_ROW,
37
+ PrefabProto::ConfigRow.new(
38
+ project_env_id: TEST_ENV_ID,
39
+ values: [
40
+ PrefabProto::ConditionalValue.new(
41
+ criteria: [
42
+ PrefabProto::Criterion.new(
43
+ operator: PrefabProto::Criterion::CriterionOperator::HIERARCHICAL_MATCH,
44
+ value_to_match: PrefabProto::ConfigValue.new(string: 'projectB.subprojectX'),
45
+ property_name: Prefab::CriteriaEvaluator::NAMESPACE_KEY
46
+ )
47
+ ],
48
+ value: PrefabProto::ConfigValue.new(string: 'projectB.subprojectX')
49
+ ),
50
+ PrefabProto::ConditionalValue.new(
51
+ criteria: [
52
+ PrefabProto::Criterion.new(
53
+ operator: PrefabProto::Criterion::CriterionOperator::HIERARCHICAL_MATCH,
54
+ value_to_match: PrefabProto::ConfigValue.new(string: 'projectB.subprojectY'),
55
+ property_name: Prefab::CriteriaEvaluator::NAMESPACE_KEY
56
+ )
57
+ ],
58
+ value: PrefabProto::ConfigValue.new(string: 'projectB.subprojectY')
59
+ ),
60
+ PrefabProto::ConditionalValue.new(
61
+ criteria: [
62
+ PrefabProto::Criterion.new(
63
+ operator: PrefabProto::Criterion::CriterionOperator::HIERARCHICAL_MATCH,
64
+ value_to_match: PrefabProto::ConfigValue.new(string: 'projectA'),
65
+ property_name: Prefab::CriteriaEvaluator::NAMESPACE_KEY
66
+ )
67
+ ],
68
+ value: PrefabProto::ConfigValue.new(string: 'valueA')
69
+ ),
70
+ PrefabProto::ConditionalValue.new(
71
+ criteria: [
72
+ PrefabProto::Criterion.new(
73
+ operator: PrefabProto::Criterion::CriterionOperator::HIERARCHICAL_MATCH,
74
+ value_to_match: PrefabProto::ConfigValue.new(string: 'projectB'),
75
+ property_name: Prefab::CriteriaEvaluator::NAMESPACE_KEY
76
+ )
77
+ ],
78
+ value: PrefabProto::ConfigValue.new(string: 'valueB')
79
+ ),
80
+ PrefabProto::ConditionalValue.new(
81
+ criteria: [
82
+ PrefabProto::Criterion.new(
83
+ operator: PrefabProto::Criterion::CriterionOperator::HIERARCHICAL_MATCH,
84
+ value_to_match: PrefabProto::ConfigValue.new(string: 'projectB.subprojectX'),
85
+ property_name: Prefab::CriteriaEvaluator::NAMESPACE_KEY
86
+ )
87
+ ],
88
+ value: PrefabProto::ConfigValue.new(string: 'projectB.subprojectX')
89
+ ),
90
+ PrefabProto::ConditionalValue.new(
91
+ criteria: [
92
+ PrefabProto::Criterion.new(
93
+ operator: PrefabProto::Criterion::CriterionOperator::HIERARCHICAL_MATCH,
94
+ value_to_match: PrefabProto::ConfigValue.new(string: 'projectB.subprojectY'),
95
+ property_name: Prefab::CriteriaEvaluator::NAMESPACE_KEY
96
+ )
97
+ ],
98
+ value: PrefabProto::ConfigValue.new(string: 'projectB.subprojectY')
99
+ ),
100
+ PrefabProto::ConditionalValue.new(
101
+ value: PrefabProto::ConfigValue.new(string: 'value_none')
102
+ )
103
+ ]
104
+ )
105
+
106
+ ]
107
+ ) },
108
+ 'key2' => { config: PrefabProto::Config.new(
109
+ key: 'key2',
110
+ rows: [
111
+ PrefabProto::ConfigRow.new(
112
+ values: [
113
+ PrefabProto::ConditionalValue.new(
114
+ value: PrefabProto::ConfigValue.new(string: 'valueB2')
115
+ )
116
+ ]
117
+ )
118
+ ]
119
+ ) }
120
+ }
121
+
122
+ @loader.stub :calc_config, loaded_values do
123
+ @resolverA = resolver_for_namespace('', @loader, project_env_id: PRODUCTION_ENV_ID)
124
+ assert_equal_context_and_jit DEFAULT_VALUE, @resolverA, 'key', {}
125
+
126
+ ## below here in the test env
127
+ @resolverA = resolver_for_namespace('', @loader)
128
+ assert_equal_context_and_jit 'value_none', @resolverA, 'key', {}
129
+
130
+ @resolverA = resolver_for_namespace('projectA', @loader)
131
+ assert_equal_context_and_jit 'valueA', @resolverA, 'key', {}
132
+
133
+ @resolverB = resolver_for_namespace('projectB', @loader)
134
+ assert_equal_context_and_jit 'valueB', @resolverB, 'key', {}
135
+
136
+ @resolverBX = resolver_for_namespace('projectB.subprojectX', @loader)
137
+ assert_equal_context_and_jit 'projectB.subprojectX', @resolverBX, 'key', {}
138
+
139
+ @resolverBX = resolver_for_namespace('projectB.subprojectX', @loader)
140
+ assert_equal_context_and_jit 'valueB2', @resolverBX, 'key2', {}
141
+
142
+ @resolverUndefinedSubProject = resolver_for_namespace('projectB.subprojectX.subsubQ',
143
+ @loader)
144
+ assert_equal_context_and_jit 'projectB.subprojectX', @resolverUndefinedSubProject, 'key', {}
145
+
146
+ @resolverBX = resolver_for_namespace('projectC', @loader)
147
+ assert_equal_context_and_jit 'value_none', @resolverBX, 'key', {}
148
+
149
+ assert_nil @resolverBX.get('key_that_doesnt_exist', nil)
150
+
151
+ assert_equal @resolverBX.to_s.strip.split("\n").map(&:strip), [
152
+ 'key | value_none | String | Match: | Source:',
153
+ 'key2 | valueB2 | String | Match: | Source:'
154
+ ]
155
+
156
+ assert_equal @resolverBX.presenter.to_h, {
157
+ 'key' => Prefab::ResolvedConfigPresenter::ConfigRow.new('key', 'value_none', nil, nil),
158
+ 'key2' => Prefab::ResolvedConfigPresenter::ConfigRow.new('key2', 'valueB2', nil, nil)
159
+ }
160
+
161
+ resolved_lines = []
162
+ @resolverBX.presenter.each do |key, row|
163
+ resolved_lines << [key, row.value]
164
+ end
165
+ assert_equal resolved_lines, [%w[key value_none], %w[key2 valueB2]]
166
+ end
167
+ end
168
+
169
+ def test_resolving_in_segment
170
+ segment_config = PrefabProto::Config.new(
171
+ config_type: PrefabProto::ConfigType::SEGMENT,
172
+ key: SEGMENT_KEY,
173
+ rows: [
174
+ PrefabProto::ConfigRow.new(
175
+ values: [
176
+ PrefabProto::ConditionalValue.new(
177
+ value: PrefabProto::ConfigValue.new(bool: true),
178
+ criteria: [
179
+ PrefabProto::Criterion.new(
180
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
181
+ value_to_match: string_list(['hotmail.com', 'gmail.com']),
182
+ property_name: 'user.email'
183
+ )
184
+ ]
185
+ ),
186
+ PrefabProto::ConditionalValue.new(value: PrefabProto::ConfigValue.new(bool: false))
187
+ ]
188
+ )
189
+ ]
190
+ )
191
+
192
+ config = PrefabProto::Config.new(
193
+ key: CONFIG_KEY,
194
+ rows: [
195
+ # wrong env
196
+ PrefabProto::ConfigRow.new(
197
+ project_env_id: TEST_ENV_ID,
198
+ values: [
199
+ PrefabProto::ConditionalValue.new(
200
+ criteria: [
201
+ PrefabProto::Criterion.new(
202
+ operator: PrefabProto::Criterion::CriterionOperator::IN_SEG,
203
+ value_to_match: PrefabProto::ConfigValue.new(string: SEGMENT_KEY)
204
+ )
205
+ ],
206
+ value: PrefabProto::ConfigValue.new(string: WRONG_ENV_VALUE)
207
+ ),
208
+ PrefabProto::ConditionalValue.new(
209
+ criteria: [],
210
+ value: PrefabProto::ConfigValue.new(string: DEFAULT_VALUE)
211
+ )
212
+ ]
213
+ ),
214
+
215
+ # correct env
216
+ PrefabProto::ConfigRow.new(
217
+ project_env_id: PRODUCTION_ENV_ID,
218
+ values: [
219
+ PrefabProto::ConditionalValue.new(
220
+ criteria: [
221
+ PrefabProto::Criterion.new(
222
+ operator: PrefabProto::Criterion::CriterionOperator::IN_SEG,
223
+ value_to_match: PrefabProto::ConfigValue.new(string: SEGMENT_KEY)
224
+ )
225
+ ],
226
+ value: PrefabProto::ConfigValue.new(string: IN_SEGMENT_VALUE)
227
+ ),
228
+ PrefabProto::ConditionalValue.new(
229
+ criteria: [],
230
+ value: PrefabProto::ConfigValue.new(string: DEFAULT_VALUE)
231
+ )
232
+ ]
233
+ )
234
+ ]
235
+ )
236
+
237
+ loaded_values = {
238
+ SEGMENT_KEY => { config: segment_config },
239
+ CONFIG_KEY => { config: config }
240
+ }
241
+
242
+ loader = MockConfigLoader.new
243
+
244
+ loader.stub :calc_config, loaded_values do
245
+ options = Prefab::Options.new
246
+ resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
247
+ resolver.project_env_id = PRODUCTION_ENV_ID
248
+
249
+ assert_equal_context_and_jit DEFAULT_VALUE, resolver, CONFIG_KEY,
250
+ { user: { email: 'test@something-else.com' } }
251
+ assert_equal_context_and_jit IN_SEGMENT_VALUE, resolver, CONFIG_KEY,
252
+ { user: { email: 'test@hotmail.com' } }
253
+ end
254
+ end
255
+
256
+ def test_resolving_not_in_segment
257
+ segment_config = PrefabProto::Config.new(
258
+ config_type: PrefabProto::ConfigType::SEGMENT,
259
+ key: SEGMENT_KEY,
260
+ rows: [
261
+ PrefabProto::ConfigRow.new(
262
+ values: [
263
+ PrefabProto::ConditionalValue.new(
264
+ value: PrefabProto::ConfigValue.new(bool: true),
265
+ criteria: [
266
+ PrefabProto::Criterion.new(
267
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
268
+ value_to_match: string_list(['hotmail.com', 'gmail.com']),
269
+ property_name: 'user.email'
270
+ )
271
+ ]
272
+ ),
273
+ PrefabProto::ConditionalValue.new(value: PrefabProto::ConfigValue.new(bool: false))
274
+ ]
275
+ )
276
+ ]
277
+ )
278
+
279
+ config = PrefabProto::Config.new(
280
+ key: CONFIG_KEY,
281
+ rows: [
282
+ PrefabProto::ConfigRow.new(
283
+ values: [
284
+ PrefabProto::ConditionalValue.new(
285
+ criteria: [
286
+ PrefabProto::Criterion.new(
287
+ operator: PrefabProto::Criterion::CriterionOperator::IN_SEG,
288
+ value_to_match: PrefabProto::ConfigValue.new(string: SEGMENT_KEY)
289
+ )
290
+ ],
291
+ value: PrefabProto::ConfigValue.new(string: IN_SEGMENT_VALUE)
292
+ ),
293
+ PrefabProto::ConditionalValue.new(
294
+ criteria: [
295
+ PrefabProto::Criterion.new(
296
+ operator: PrefabProto::Criterion::CriterionOperator::NOT_IN_SEG,
297
+ value_to_match: PrefabProto::ConfigValue.new(string: SEGMENT_KEY)
298
+ )
299
+ ],
300
+ value: PrefabProto::ConfigValue.new(string: NOT_IN_SEGMENT_VALUE)
301
+ )
302
+ ]
303
+ )
304
+ ]
305
+ )
306
+
307
+ loaded_values = {
308
+ SEGMENT_KEY => { config: segment_config },
309
+ CONFIG_KEY => { config: config }
310
+ }
311
+
312
+ loader = MockConfigLoader.new
313
+
314
+ loader.stub :calc_config, loaded_values do
315
+ options = Prefab::Options.new
316
+ resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
317
+
318
+ assert_equal_context_and_jit IN_SEGMENT_VALUE, resolver, CONFIG_KEY, { user: { email: 'test@hotmail.com' } }
319
+ assert_equal_context_and_jit NOT_IN_SEGMENT_VALUE, resolver, CONFIG_KEY, { user: { email: 'test@something-else.com' } }
320
+ end
321
+ end
322
+
323
+ def test_jit_context_merges_with_existing_context
324
+ config = PrefabProto::Config.new(
325
+ key: CONFIG_KEY,
326
+ rows: [
327
+ DEFAULT_ROW,
328
+ PrefabProto::ConfigRow.new(
329
+ project_env_id: TEST_ENV_ID,
330
+ values: [
331
+ PrefabProto::ConditionalValue.new(
332
+ criteria: [
333
+ PrefabProto::Criterion.new(
334
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_IS_ONE_OF,
335
+ value_to_match: string_list(%w[pro advanced]),
336
+ property_name: 'team.plan'
337
+ ),
338
+
339
+ PrefabProto::Criterion.new(
340
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
341
+ value_to_match: string_list(%w[@example.com]),
342
+ property_name: 'user.email'
343
+ )
344
+ ],
345
+ value: PrefabProto::ConfigValue.new(string: DESIRED_VALUE)
346
+ )
347
+ ]
348
+ )
349
+ ]
350
+ )
351
+
352
+ loader = MockConfigLoader.new
353
+
354
+ loader.stub :calc_config, { CONFIG_KEY => { config: config } } do
355
+ options = Prefab::Options.new
356
+ resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
357
+ resolver.project_env_id = TEST_ENV_ID
358
+
359
+ Prefab::Context.with_context({ user: { email: 'test@example.com' } }) do
360
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY).unwrapped_value
361
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY, { team: { plan: 'freebie' } }).unwrapped_value
362
+ assert_equal DESIRED_VALUE, resolver.get(CONFIG_KEY, { team: { plan: 'pro' } }).unwrapped_value
363
+ end
364
+ end
365
+ end
366
+
367
+ def test_jit_can_clobber_existing_context
368
+ config = PrefabProto::Config.new(
369
+ key: CONFIG_KEY,
370
+ rows: [
371
+ DEFAULT_ROW,
372
+ PrefabProto::ConfigRow.new(
373
+ project_env_id: TEST_ENV_ID,
374
+ values: [
375
+ PrefabProto::ConditionalValue.new(
376
+ criteria: [
377
+ PrefabProto::Criterion.new(
378
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_IS_ONE_OF,
379
+ value_to_match: string_list(%w[pro advanced]),
380
+ property_name: 'team.plan'
381
+ ),
382
+
383
+ PrefabProto::Criterion.new(
384
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
385
+ value_to_match: string_list(%w[@example.com]),
386
+ property_name: 'user.email'
387
+ )
388
+ ],
389
+ value: PrefabProto::ConfigValue.new(string: DESIRED_VALUE)
390
+ )
391
+ ]
392
+ )
393
+ ]
394
+ )
395
+
396
+ loader = MockConfigLoader.new
397
+
398
+ loader.stub :calc_config, { CONFIG_KEY => { config: config } } do
399
+ options = Prefab::Options.new
400
+ resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
401
+ resolver.project_env_id = TEST_ENV_ID
402
+
403
+ Prefab::Context.with_context({ user: { email: 'test@hotmail.com' }, team: { plan: 'pro' } }) do
404
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY).unwrapped_value
405
+ assert_equal DESIRED_VALUE, resolver.get(CONFIG_KEY, { user: { email: 'test@example.com' } }).unwrapped_value
406
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY, { team: { plan: 'freebie' } }).unwrapped_value
407
+ end
408
+ end
409
+ end
410
+
411
+ private
412
+
413
+ def resolver_for_namespace(namespace, loader, project_env_id: TEST_ENV_ID)
414
+ options = Prefab::Options.new(
415
+ namespace: namespace
416
+ )
417
+ resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
418
+ resolver.project_env_id = project_env_id
419
+ resolver.update
420
+ resolver
421
+ end
422
+
423
+ def assert_equal_context_and_jit(expected_value, resolver, key, properties)
424
+ assert_equal expected_value, resolver.get(key, properties).unwrapped_value
425
+
426
+ Prefab::Context.with_context(properties) do
427
+ assert_equal expected_value, resolver.get(key).unwrapped_value
428
+ end
429
+ end
430
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigValueUnwrapper < Minitest::Test
6
+ CONFIG = PrefabProto::Config.new(
7
+ key: 'config_key'
8
+ )
9
+ EMPTY_CONTEXT = Prefab::Context.new()
10
+ DECRYPTION_KEY_NAME = "decryption.key"
11
+ DECRYPTION_KEY_VALUE = Prefab::Encryption.generate_new_hex_key
12
+
13
+ def setup
14
+ super
15
+ @mock_resolver = MockResolver.new
16
+ end
17
+
18
+ def test_unwrapping_int
19
+ config_value = PrefabProto::ConfigValue.new(int: 123)
20
+ assert_equal 123, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
21
+ end
22
+
23
+ def test_unwrapping_string
24
+ config_value = PrefabProto::ConfigValue.new(string: 'abc')
25
+ assert_equal 'abc', unwrap(config_value, CONFIG, EMPTY_CONTEXT)
26
+ assert_equal 'abc', reportable_value(config_value, CONFIG, EMPTY_CONTEXT)
27
+ end
28
+
29
+ def test_unwrapping_double
30
+ config_value = PrefabProto::ConfigValue.new(double: 1.23)
31
+ assert_equal 1.23, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
32
+ end
33
+
34
+ def test_unwrapping_bool
35
+ config_value = PrefabProto::ConfigValue.new(bool: true)
36
+ assert_equal true, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
37
+
38
+ config_value = PrefabProto::ConfigValue.new(bool: false)
39
+ assert_equal false, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
40
+ end
41
+
42
+ def test_unwrapping_log_level
43
+ config_value = PrefabProto::ConfigValue.new(log_level: :INFO)
44
+ assert_equal :INFO, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
45
+ end
46
+
47
+ def test_unwrapping_string_list
48
+ config_value = PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: %w[a b c]))
49
+ assert_equal %w[a b c], unwrap(config_value, CONFIG, EMPTY_CONTEXT)
50
+ end
51
+
52
+ def test_unwrapping_weighted_values
53
+ # single value
54
+ config_value = PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 1]]))
55
+
56
+ assert_equal 'abc', unwrap(config_value, CONFIG, EMPTY_CONTEXT)
57
+
58
+ # multiple values, evenly distributed
59
+ config_value = PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 1], ['def', 1], ['ghi', 1]]))
60
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:000'))
61
+ assert_equal 'ghi', unwrap(config_value, CONFIG, context_with_key('user:456'))
62
+ assert_equal 'abc', unwrap(config_value, CONFIG, context_with_key('user:789'))
63
+ assert_equal 'ghi', unwrap(config_value, CONFIG, context_with_key('user:888'))
64
+
65
+ # multiple values, unevenly distributed
66
+ config_value = PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 1], ['def', 99], ['ghi', 1]]))
67
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:123'))
68
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:456'))
69
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:789'))
70
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:012'))
71
+ assert_equal 'ghi', unwrap(config_value, CONFIG, context_with_key('user:428'))
72
+ assert_equal 'abc', unwrap(config_value, CONFIG, context_with_key('user:548'))
73
+ end
74
+
75
+ def test_unwrapping_provided_values
76
+ with_env('ENV_VAR_NAME', 'unit test value')do
77
+ value = PrefabProto::Provided.new(
78
+ source: :ENV_VAR,
79
+ lookup: "ENV_VAR_NAME"
80
+ )
81
+ config_value = PrefabProto::ConfigValue.new(provided: value)
82
+ assert_equal 'unit test value', unwrap(config_value, CONFIG, EMPTY_CONTEXT)
83
+ end
84
+ end
85
+
86
+ def test_unwrapping_provided_values_of_type_string_list
87
+ with_env('ENV_VAR_NAME', '["bob","cary"]')do
88
+ value = PrefabProto::Provided.new(
89
+ source: :ENV_VAR,
90
+ lookup: "ENV_VAR_NAME"
91
+ )
92
+ config_value = PrefabProto::ConfigValue.new(provided: value)
93
+ assert_equal ["bob", "cary"], unwrap(config_value, CONFIG, EMPTY_CONTEXT)
94
+ end
95
+ end
96
+
97
+ def test_unwrapping_provided_values_coerces_to_int
98
+ with_env('ENV_VAR_NAME', '42')do
99
+ value = PrefabProto::Provided.new(
100
+ source: :ENV_VAR,
101
+ lookup: "ENV_VAR_NAME"
102
+ )
103
+ config_value = PrefabProto::ConfigValue.new(provided: value)
104
+ assert_equal 42, unwrap(config_value, config_of(PrefabProto::Config::ValueType::INT), EMPTY_CONTEXT)
105
+ end
106
+ end
107
+
108
+ def test_unwrapping_provided_values_when_value_type_mismatch
109
+ with_env('ENV_VAR_NAME', 'not an int')do
110
+ value = PrefabProto::Provided.new(
111
+ source: :ENV_VAR,
112
+ lookup: "ENV_VAR_NAME"
113
+ )
114
+ config_value = PrefabProto::ConfigValue.new(provided: value)
115
+
116
+ assert_raises Prefab::Errors::EnvVarParseError do
117
+ unwrap(config_value, config_of(PrefabProto::Config::ValueType::INT), EMPTY_CONTEXT)
118
+ end
119
+ end
120
+ end
121
+
122
+ def test_coerce
123
+ assert_equal "string", Prefab::ConfigValueUnwrapper.coerce_into_type("string", CONFIG, "ENV")
124
+ assert_equal 42, Prefab::ConfigValueUnwrapper.coerce_into_type("42", CONFIG, "ENV")
125
+ assert_equal false, Prefab::ConfigValueUnwrapper.coerce_into_type("false", CONFIG, "ENV")
126
+ assert_equal 42.42, Prefab::ConfigValueUnwrapper.coerce_into_type("42.42", CONFIG, "ENV")
127
+ assert_equal ["a","b"], Prefab::ConfigValueUnwrapper.coerce_into_type("['a','b']", CONFIG, "ENV")
128
+
129
+ assert_equal "string", Prefab::ConfigValueUnwrapper.coerce_into_type("string", config_of(PrefabProto::Config::ValueType::STRING),"ENV")
130
+ assert_equal "42", Prefab::ConfigValueUnwrapper.coerce_into_type("42", config_of(PrefabProto::Config::ValueType::STRING),"ENV")
131
+ assert_equal "42.42", Prefab::ConfigValueUnwrapper.coerce_into_type("42.42", config_of(PrefabProto::Config::ValueType::STRING),"ENV")
132
+ assert_equal 42, Prefab::ConfigValueUnwrapper.coerce_into_type("42", config_of(PrefabProto::Config::ValueType::INT),"ENV")
133
+ assert_equal false, Prefab::ConfigValueUnwrapper.coerce_into_type("false", config_of(PrefabProto::Config::ValueType::BOOL),"ENV")
134
+ assert_equal 42.42, Prefab::ConfigValueUnwrapper.coerce_into_type("42.42", config_of(PrefabProto::Config::ValueType::DOUBLE),"ENV")
135
+ assert_equal ["a","b"], Prefab::ConfigValueUnwrapper.coerce_into_type("['a','b']", config_of(PrefabProto::Config::ValueType::STRING_LIST),"ENV")
136
+
137
+ assert_raises Prefab::Errors::EnvVarParseError do
138
+ Prefab::ConfigValueUnwrapper.coerce_into_type("not an int", config_of(PrefabProto::Config::ValueType::INT), "ENV")
139
+ end
140
+ assert_raises Prefab::Errors::EnvVarParseError do
141
+ Prefab::ConfigValueUnwrapper.coerce_into_type("not bool", config_of(PrefabProto::Config::ValueType::BOOL), "ENV")
142
+ end
143
+ assert_raises Prefab::Errors::EnvVarParseError do
144
+ Prefab::ConfigValueUnwrapper.coerce_into_type("not a double", config_of(PrefabProto::Config::ValueType::DOUBLE), "ENV")
145
+ end
146
+ assert_raises Prefab::Errors::EnvVarParseError do
147
+ Prefab::ConfigValueUnwrapper.coerce_into_type("not a list", config_of(PrefabProto::Config::ValueType::STRING_LIST), "ENV")
148
+ end
149
+ end
150
+
151
+ def test_unwrapping_provided_values_with_missing_env_var
152
+ value = PrefabProto::Provided.new(
153
+ source: :ENV_VAR,
154
+ lookup: "NON_EXISTENT_ENV_VAR_NAME"
155
+ )
156
+ config_value = PrefabProto::ConfigValue.new(provided: value)
157
+ assert_raises(Prefab::Errors::MissingEnvVarError) do
158
+ unwrap(config_value, CONFIG, EMPTY_CONTEXT)
159
+ end
160
+ end
161
+
162
+ def test_unwrapping_encrypted_values_decrypts
163
+ clear_text = "very secret stuff"
164
+ encrypted = Prefab::Encryption.new(DECRYPTION_KEY_VALUE).encrypt(clear_text)
165
+ config_value = PrefabProto::ConfigValue.new(string: encrypted, decrypt_with: "decryption.key")
166
+ assert_equal clear_text, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
167
+ assert reportable_value(config_value, CONFIG, EMPTY_CONTEXT).start_with? Prefab::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
168
+ end
169
+
170
+ def test_confidential
171
+ config_value = PrefabProto::ConfigValue.new(confidential: true, string: "something confidential")
172
+ assert reportable_value(config_value, CONFIG, EMPTY_CONTEXT).start_with? Prefab::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
173
+ end
174
+
175
+ def test_unwrap_confiential_provided
176
+ with_env('PAAS_PASSWORD', "the password")do
177
+ value = PrefabProto::Provided.new(
178
+ source: :ENV_VAR,
179
+ lookup: "PAAS_PASSWORD"
180
+ )
181
+ config_value = PrefabProto::ConfigValue.new(provided: value, confidential: true)
182
+ assert_equal "the password", unwrap(config_value, CONFIG, EMPTY_CONTEXT)
183
+ assert reportable_value(config_value, CONFIG, EMPTY_CONTEXT).start_with? Prefab::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ def config_of(value_type)
190
+ PrefabProto::Config.new(
191
+ key: 'config-key',
192
+ value_type: value_type
193
+ )
194
+ end
195
+
196
+ def context_with_key(key)
197
+ Prefab::Context.new(user: { key: key })
198
+ end
199
+
200
+ def unwrap(config_value, config_key, context)
201
+ Prefab::ConfigValueUnwrapper.deepest_value(config_value, config_key, context, @mock_resolver).unwrap
202
+ end
203
+
204
+ def reportable_value(config_value, config_key, context)
205
+ Prefab::ConfigValueUnwrapper.deepest_value(config_value, config_key, context, @mock_resolver).reportable_value
206
+ end
207
+
208
+ class MockResolver
209
+ def get(key)
210
+ if DECRYPTION_KEY_NAME == key
211
+ Prefab::Evaluation.new(config: PrefabProto::Config.new(key: key),
212
+ value: PrefabProto::ConfigValue.new(string: DECRYPTION_KEY_VALUE),
213
+ value_index: 0,
214
+ config_row_index: 0,
215
+ context: Prefab::Context.new,
216
+ resolver: self
217
+ )
218
+
219
+ else
220
+ raise "unexpected key"
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigValueWrapper < Minitest::Test
6
+ def test_wrap_integer
7
+ result = Prefab::ConfigValueWrapper.wrap(42)
8
+ assert_instance_of PrefabProto::ConfigValue, result
9
+ assert_equal 42, result.int
10
+ end
11
+
12
+ def test_wrap_float
13
+ result = Prefab::ConfigValueWrapper.wrap(3.14)
14
+ assert_instance_of PrefabProto::ConfigValue, result
15
+ assert_equal 3.14, result.double
16
+ end
17
+
18
+ def test_wrap_boolean_true
19
+ result = Prefab::ConfigValueWrapper.wrap(true)
20
+ assert_instance_of PrefabProto::ConfigValue, result
21
+ assert_equal true, result.bool
22
+ end
23
+
24
+ def test_wrap_boolean_false
25
+ result = Prefab::ConfigValueWrapper.wrap(false)
26
+ assert_instance_of PrefabProto::ConfigValue, result
27
+ assert_equal false, result.bool
28
+ end
29
+
30
+ def test_wrap_array
31
+ result = Prefab::ConfigValueWrapper.wrap(['one', 'two', 'three'])
32
+ assert_instance_of PrefabProto::ConfigValue, result
33
+ assert_instance_of PrefabProto::StringList, result.string_list
34
+ assert_equal ['one', 'two', 'three'], result.string_list.values
35
+ end
36
+
37
+ def test_wrap_string
38
+ result = Prefab::ConfigValueWrapper.wrap('hello')
39
+ assert_instance_of PrefabProto::ConfigValue, result
40
+ assert_equal 'hello', result.string
41
+ end
42
+ end