sdk-reforge 1.9.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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/pull_request_template.md +8 -0
  5. data/.github/workflows/ruby.yml +48 -0
  6. data/.gitmodules +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.tool-versions +1 -0
  9. data/CHANGELOG.md +257 -0
  10. data/CODEOWNERS +1 -0
  11. data/Gemfile +29 -0
  12. data/Gemfile.lock +182 -0
  13. data/LICENSE.txt +20 -0
  14. data/README.md +105 -0
  15. data/Rakefile +63 -0
  16. data/VERSION +1 -0
  17. data/compile_protos.sh +20 -0
  18. data/dev/allocation_stats +60 -0
  19. data/dev/benchmark +40 -0
  20. data/dev/console +12 -0
  21. data/dev/script_setup.rb +18 -0
  22. data/lib/prefab_pb.rb +77 -0
  23. data/lib/reforge/caching_http_connection.rb +95 -0
  24. data/lib/reforge/client.rb +133 -0
  25. data/lib/reforge/config_client.rb +275 -0
  26. data/lib/reforge/config_client_presenter.rb +18 -0
  27. data/lib/reforge/config_loader.rb +67 -0
  28. data/lib/reforge/config_resolver.rb +84 -0
  29. data/lib/reforge/config_value_unwrapper.rb +123 -0
  30. data/lib/reforge/config_value_wrapper.rb +18 -0
  31. data/lib/reforge/context.rb +241 -0
  32. data/lib/reforge/context_shape.rb +20 -0
  33. data/lib/reforge/context_shape_aggregator.rb +70 -0
  34. data/lib/reforge/criteria_evaluator.rb +345 -0
  35. data/lib/reforge/duration.rb +58 -0
  36. data/lib/reforge/encryption.rb +65 -0
  37. data/lib/reforge/error.rb +6 -0
  38. data/lib/reforge/errors/env_var_parse_error.rb +11 -0
  39. data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/reforge/errors/missing_default_error.rb +13 -0
  42. data/lib/reforge/errors/missing_env_var_error.rb +11 -0
  43. data/lib/reforge/errors/uninitialized_error.rb +13 -0
  44. data/lib/reforge/evaluation.rb +53 -0
  45. data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
  46. data/lib/reforge/example_contexts_aggregator.rb +77 -0
  47. data/lib/reforge/exponential_backoff.rb +21 -0
  48. data/lib/reforge/feature_flag_client.rb +43 -0
  49. data/lib/reforge/fixed_size_hash.rb +14 -0
  50. data/lib/reforge/http_connection.rb +45 -0
  51. data/lib/reforge/internal_logger.rb +43 -0
  52. data/lib/reforge/javascript_stub.rb +99 -0
  53. data/lib/reforge/local_config_parser.rb +151 -0
  54. data/lib/reforge/murmer3.rb +50 -0
  55. data/lib/reforge/options.rb +191 -0
  56. data/lib/reforge/periodic_sync.rb +74 -0
  57. data/lib/reforge/prefab.rb +120 -0
  58. data/lib/reforge/rate_limit_cache.rb +41 -0
  59. data/lib/reforge/resolved_config_presenter.rb +86 -0
  60. data/lib/reforge/semver.rb +132 -0
  61. data/lib/reforge/sse_config_client.rb +112 -0
  62. data/lib/reforge/time_helpers.rb +7 -0
  63. data/lib/reforge/weighted_value_resolver.rb +42 -0
  64. data/lib/reforge/yaml_config_parser.rb +34 -0
  65. data/lib/reforge-sdk.rb +57 -0
  66. data/test/fixtures/datafile.json +87 -0
  67. data/test/integration_test.rb +171 -0
  68. data/test/integration_test_helpers.rb +114 -0
  69. data/test/support/common_helpers.rb +201 -0
  70. data/test/support/mock_base_client.rb +41 -0
  71. data/test/support/mock_config_client.rb +19 -0
  72. data/test/support/mock_config_loader.rb +1 -0
  73. data/test/test_caching_http_connection.rb +218 -0
  74. data/test/test_client.rb +351 -0
  75. data/test/test_config_client.rb +84 -0
  76. data/test/test_config_loader.rb +82 -0
  77. data/test/test_config_resolver.rb +502 -0
  78. data/test/test_config_value_unwrapper.rb +270 -0
  79. data/test/test_config_value_wrapper.rb +42 -0
  80. data/test/test_context.rb +271 -0
  81. data/test/test_context_shape.rb +50 -0
  82. data/test/test_context_shape_aggregator.rb +150 -0
  83. data/test/test_criteria_evaluator.rb +1180 -0
  84. data/test/test_duration.rb +37 -0
  85. data/test/test_encryption.rb +16 -0
  86. data/test/test_evaluation_summary_aggregator.rb +162 -0
  87. data/test/test_example_contexts_aggregator.rb +233 -0
  88. data/test/test_exponential_backoff.rb +18 -0
  89. data/test/test_feature_flag_client.rb +16 -0
  90. data/test/test_fixed_size_hash.rb +119 -0
  91. data/test/test_helper.rb +17 -0
  92. data/test/test_integration.rb +75 -0
  93. data/test/test_internal_logger.rb +25 -0
  94. data/test/test_javascript_stub.rb +176 -0
  95. data/test/test_local_config_parser.rb +147 -0
  96. data/test/test_logger_initialization.rb +12 -0
  97. data/test/test_options.rb +93 -0
  98. data/test/test_prefab.rb +16 -0
  99. data/test/test_rate_limit_cache.rb +44 -0
  100. data/test/test_semver.rb +108 -0
  101. data/test/test_sse_config_client.rb +211 -0
  102. data/test/test_weighted_value_resolver.rb +71 -0
  103. metadata +345 -0
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigLoader < Minitest::Test
6
+ def setup
7
+ super
8
+ options = Reforge::Options.new(
9
+ )
10
+ @loader = Reforge::ConfigLoader.new(MockBaseClient.new(options))
11
+ end
12
+
13
+
14
+
15
+
16
+
17
+ def test_highwater
18
+ assert_equal 0, @loader.highwater_mark
19
+ @loader.set(PrefabProto::Config.new(id: 1, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 456))]),
20
+ 'test')
21
+ assert_equal 1, @loader.highwater_mark
22
+
23
+ @loader.set(PrefabProto::Config.new(id: 5, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 456))]),
24
+ 'test')
25
+ assert_equal 5, @loader.highwater_mark
26
+ @loader.set(PrefabProto::Config.new(id: 2, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 456))]),
27
+ 'test')
28
+ assert_equal 5, @loader.highwater_mark
29
+ end
30
+
31
+ def test_keeps_most_recent
32
+ assert_equal 0, @loader.highwater_mark
33
+ @loader.set(PrefabProto::Config.new(id: 1, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 1))]),
34
+ 'test')
35
+ assert_equal 1, @loader.highwater_mark
36
+ should_be :int, 1, 'sample_int'
37
+
38
+ @loader.set(PrefabProto::Config.new(id: 4, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 4))]),
39
+ 'test')
40
+ assert_equal 4, @loader.highwater_mark
41
+ should_be :int, 4, 'sample_int'
42
+
43
+ @loader.set(PrefabProto::Config.new(id: 2, key: 'sample_int', rows: [config_row(PrefabProto::ConfigValue.new(int: 2))]),
44
+ 'test')
45
+ assert_equal 4, @loader.highwater_mark
46
+ should_be :int, 4, 'sample_int'
47
+ end
48
+
49
+
50
+ def test_api_deltas
51
+ val = PrefabProto::ConfigValue.new(int: 456)
52
+ config = PrefabProto::Config.new(key: 'sample_int', rows: [config_row(val)])
53
+ @loader.set(config, 'test')
54
+
55
+ configs = PrefabProto::Configs.new
56
+ configs.configs << config
57
+ assert_equal configs, @loader.get_api_deltas
58
+ end
59
+
60
+ def test_loading_tombstones_removes_entries
61
+ val = PrefabProto::ConfigValue.new(int: 456)
62
+ config = PrefabProto::Config.new(key: 'sample_int', rows: [config_row(val)], id: 2)
63
+ @loader.set(config, 'test')
64
+
65
+ config = PrefabProto::Config.new(key: 'sample_int', rows: [], id: 3)
66
+ @loader.set(config, 'test')
67
+
68
+ configs = PrefabProto::Configs.new
69
+ assert_equal configs, @loader.get_api_deltas
70
+ end
71
+
72
+ private
73
+
74
+ def should_be(type, value, key)
75
+ assert_equal type, @loader.calc_config[key][:config].rows[0].values[0].value.type
76
+ assert_equal value, @loader.calc_config[key][:config].rows[0].values[0].value.send(type)
77
+ end
78
+
79
+ def config_row(value)
80
+ PrefabProto::ConfigRow.new(values: [PrefabProto::ConditionalValue.new(value: value)])
81
+ end
82
+ end
@@ -0,0 +1,502 @@
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: Reforge::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: Reforge::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: Reforge::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: Reforge::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: Reforge::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: Reforge::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' => Reforge::ResolvedConfigPresenter::ConfigRow.new('key', 'value_none', nil, nil),
158
+ 'key2' => Reforge::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 = Reforge::Options.new
246
+ resolver = Reforge::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 = Reforge::Options.new
316
+ resolver = Reforge::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 = Reforge::Options.new
356
+ resolver = Reforge::ConfigResolver.new(MockBaseClient.new(options), loader)
357
+ resolver.project_env_id = TEST_ENV_ID
358
+
359
+ Reforge::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 = Reforge::Options.new
400
+ resolver = Reforge::ConfigResolver.new(MockBaseClient.new(options), loader)
401
+ resolver.project_env_id = TEST_ENV_ID
402
+
403
+ Reforge::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
+ def test_context_lookup
412
+ global_context = { cpu: { count: 4, speed: '2.4GHz' }, clock: { timezone: 'UTC' } }
413
+ default_context = { 'prefab-api-key' => { 'user-id' => 123 } }
414
+ local_context = { clock: { timezone: 'PST' }, user: { name: 'Ted', email: 'ted@example.com' } }
415
+ jit_context = { user: { name: 'Frank' } }
416
+
417
+ config = PrefabProto::Config.new( key: 'example', rows: [ PrefabProto::ConfigRow.new( values: [ PrefabProto::ConditionalValue.new( value: PrefabProto::ConfigValue.new(string: 'valueB2')) ]) ])
418
+
419
+ client = new_client(global_context: global_context, config: [config])
420
+
421
+ # we fake getting the default context from the API
422
+ Reforge::Context.default_context = default_context
423
+
424
+ resolver = client.resolver
425
+
426
+ client.with_context(local_context) do
427
+ context = resolver.get("example", jit_context).context
428
+
429
+ # This digs all the way to the global context
430
+ assert_equal 4, context.get('cpu.count')
431
+ assert_equal '2.4GHz', context.get('cpu.speed')
432
+
433
+ # This digs to the default context
434
+ assert_equal 123, context.get('prefab-api-key.user-id')
435
+
436
+ # This digs to the local context
437
+ assert_equal 'PST', context.get('clock.timezone')
438
+
439
+ # This uses the jit context
440
+ assert_equal 'Frank', context.get('user.name')
441
+
442
+ # This is nil in the jit context because `user` was clobbered
443
+ assert_nil context.get('user.email')
444
+
445
+ context = resolver.get("example").context
446
+
447
+ # But without the JIT clobbering, it is still set
448
+ assert_equal 'ted@example.com', context.get('user.email')
449
+ end
450
+ end
451
+
452
+ def test_context_lookup_with_no_local_context
453
+ global_context = { cpu: { count: 4, speed: '2.4GHz' }, clock: { timezone: 'UTC' } }
454
+ default_context = { 'prefab-api-key' => { 'user-id' => 123 } }
455
+ jit_context = { user: { name: 'Frank' } }
456
+
457
+ config = PrefabProto::Config.new( key: 'example', rows: [ PrefabProto::ConfigRow.new( values: [ PrefabProto::ConditionalValue.new( value: PrefabProto::ConfigValue.new(string: 'valueB2')) ]) ])
458
+
459
+ client = new_client(global_context: global_context, config: [config])
460
+
461
+ # we fake getting the default context from the API
462
+ Reforge::Context.default_context = default_context
463
+
464
+ resolver = client.resolver
465
+
466
+ context = resolver.get("example", jit_context).context
467
+
468
+ # This digs all the way to the global context
469
+ assert_equal 4, context.get('cpu.count')
470
+ assert_equal '2.4GHz', context.get('cpu.speed')
471
+ assert_equal 'UTC', context.get('clock.timezone')
472
+
473
+ # This digs to the default context
474
+ assert_equal 123, context.get('prefab-api-key.user-id')
475
+
476
+ # This uses the jit context
477
+ assert_equal 'Frank', context.get('user.name')
478
+
479
+ # This is nil in the jit context because `user` was clobbered
480
+ assert_nil context.get('user.email')
481
+ end
482
+
483
+ private
484
+
485
+ def resolver_for_namespace(namespace, loader, project_env_id: TEST_ENV_ID)
486
+ options = Reforge::Options.new(
487
+ namespace: namespace
488
+ )
489
+ resolver = Reforge::ConfigResolver.new(MockBaseClient.new(options), loader)
490
+ resolver.project_env_id = project_env_id
491
+ resolver.update
492
+ resolver
493
+ end
494
+
495
+ def assert_equal_context_and_jit(expected_value, resolver, key, properties)
496
+ assert_equal expected_value, resolver.get(key, properties).unwrapped_value
497
+
498
+ Reforge::Context.with_context(properties) do
499
+ assert_equal expected_value, resolver.get(key).unwrapped_value
500
+ end
501
+ end
502
+ end