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,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