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,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Reforge
6
+ class CachingHttpConnectionTest < Minitest::Test
7
+ def setup
8
+ @uri = 'https://api.example.com'
9
+ @sdk_key = 'test-key'
10
+ @path = '/some/path'
11
+
12
+ # Reset the cache before each test
13
+ CachingHttpConnection.reset_cache!
14
+
15
+ # Setup the mock HTTP connection
16
+ @http_connection = Minitest::Mock.new
17
+ @http_connection.expect(:uri, @uri)
18
+
19
+ # Stub the HttpConnection constructor
20
+ HttpConnection.stub :new, @http_connection do
21
+ @subject = CachingHttpConnection.new(@uri, @sdk_key)
22
+ end
23
+ end
24
+
25
+ def test_caches_responses_with_etag_and_max_age
26
+ response_body = 'response data'
27
+ response = Faraday::Response.new(
28
+ status: 200,
29
+ body: response_body,
30
+ response_headers: {
31
+ 'ETag' => 'abc123',
32
+ 'Cache-Control' => 'max-age=60'
33
+ }
34
+ )
35
+
36
+ # Expect two calls to uri (one for each request) and one call to get
37
+ @http_connection.expect(:uri, @uri)
38
+ @http_connection.expect(:get, response, [@path])
39
+
40
+ HttpConnection.stub :new, @http_connection do
41
+ # First request should miss cache
42
+ first_response = @subject.get(@path)
43
+ assert_equal response_body, first_response.body
44
+ assert_equal 'MISS', first_response.headers['X-Cache']
45
+
46
+ # Second request should hit cache
47
+ second_response = @subject.get(@path)
48
+ assert_equal response_body, second_response.body
49
+ assert_equal 'HIT', second_response.headers['X-Cache']
50
+ end
51
+
52
+ @http_connection.verify
53
+ end
54
+
55
+ def test_respects_max_age_directive
56
+ response = Faraday::Response.new(
57
+ status: 200,
58
+ body: 'fresh data',
59
+ response_headers: {
60
+ 'ETag' => 'abc123',
61
+ 'Cache-Control' => 'max-age=60'
62
+ }
63
+ )
64
+
65
+ mock = Minitest::Mock.new
66
+ def mock.uri
67
+ 'https://api.example.com'
68
+ end
69
+
70
+ # First request
71
+ mock.expect(:get, response, [@path])
72
+ # After max-age expires, new request with etag
73
+ mock.expect(:get, response, [@path, { 'If-None-Match' => 'abc123' }])
74
+
75
+ Timecop.freeze do
76
+ subject = CachingHttpConnection.new(@uri, @sdk_key)
77
+ subject.instance_variable_set('@connection', mock)
78
+
79
+ # Initial request
80
+ subject.get(@path)
81
+
82
+ # Within max-age window
83
+ Timecop.travel(59)
84
+ cached_response = subject.get(@path)
85
+ assert_equal 'HIT', cached_response.headers['X-Cache']
86
+
87
+ # After max-age window
88
+ Timecop.travel(61)
89
+ new_response = subject.get(@path)
90
+ assert_equal 'MISS', new_response.headers['X-Cache']
91
+ end
92
+
93
+ mock.verify
94
+ end
95
+ def test_handles_304_not_modified
96
+ initial_response = Faraday::Response.new(
97
+ status: 200,
98
+ body: 'cached data',
99
+ response_headers: { 'ETag' => 'abc123' }
100
+ )
101
+
102
+ not_modified_response = Faraday::Response.new(
103
+ status: 304,
104
+ body: '',
105
+ response_headers: { 'ETag' => 'abc123' }
106
+ )
107
+
108
+ mock = Minitest::Mock.new
109
+ def mock.uri
110
+ 'https://api.example.com'
111
+ end
112
+
113
+ # First request with single arg
114
+ mock.expect(:get, initial_response, [@path])
115
+
116
+ # Second request with both path and headers
117
+ mock.expect(:get, not_modified_response, [@path, { 'If-None-Match' => 'abc123' }])
118
+
119
+ subject = CachingHttpConnection.new(@uri, @sdk_key)
120
+ subject.instance_variable_set('@connection', mock)
121
+
122
+ # Initial request to populate cache
123
+ first_response = subject.get(@path)
124
+ assert_equal 'cached data', first_response.body
125
+ assert_equal 'MISS', first_response.headers['X-Cache']
126
+
127
+ # Subsequent request gets 304
128
+ cached_response = subject.get(@path)
129
+ assert_equal 'cached data', cached_response.body
130
+ assert_equal 200, cached_response.status
131
+ assert_equal 'HIT', cached_response.headers['X-Cache']
132
+
133
+ mock.verify
134
+ end
135
+
136
+ def test_does_not_cache_no_store_responses
137
+ response = Faraday::Response.new(
138
+ status: 200,
139
+ body: 'uncacheable data',
140
+ response_headers: { 'Cache-Control' => 'no-store' }
141
+ )
142
+
143
+ mock = Minitest::Mock.new
144
+ def mock.uri
145
+ 'https://api.example.com'
146
+ end
147
+ # Both gets with single arg
148
+ mock.expect(:get, response, [@path])
149
+ mock.expect(:get, response, [@path])
150
+
151
+ subject = CachingHttpConnection.new(@uri, @sdk_key)
152
+ subject.instance_variable_set('@connection', mock)
153
+
154
+ 2.times do
155
+ result = subject.get(@path)
156
+ assert_equal 'MISS', result.headers['X-Cache']
157
+ end
158
+
159
+ mock.verify
160
+ end
161
+ def test_cache_is_shared_across_instances
162
+ HttpConnection.stub :new, @http_connection do
163
+ instance1 = CachingHttpConnection.new(@uri, @sdk_key)
164
+ instance2 = CachingHttpConnection.new(@uri, @sdk_key)
165
+
166
+ assert_same instance1.class.cache, instance2.class.cache
167
+ end
168
+ end
169
+
170
+ def test_cache_can_be_reset
171
+ old_cache = CachingHttpConnection.cache
172
+ CachingHttpConnection.reset_cache!
173
+ refute_same CachingHttpConnection.cache, old_cache
174
+ end
175
+
176
+ def test_adds_if_none_match_header_when_cached
177
+ # First response to be cached
178
+ initial_response = Faraday::Response.new(
179
+ status: 200,
180
+ body: 'cached data',
181
+ response_headers: { 'ETag' => 'abc123' }
182
+ )
183
+
184
+ # Second request should have If-None-Match header
185
+ not_modified_response = Faraday::Response.new(
186
+ status: 304,
187
+ body: '',
188
+ response_headers: { 'ETag' => 'abc123' }
189
+ )
190
+
191
+ mock = Minitest::Mock.new
192
+ def mock.uri
193
+ 'https://api.example.com'
194
+ end
195
+
196
+ # First request should not have If-None-Match
197
+ mock.expect(:get, initial_response, [@path])
198
+
199
+ # Second request should have If-None-Match header
200
+ mock.expect(:get, not_modified_response, [@path, { 'If-None-Match' => 'abc123' }])
201
+
202
+ subject = CachingHttpConnection.new(@uri, @sdk_key)
203
+ subject.instance_variable_set('@connection', mock)
204
+
205
+ # Initial request to populate cache
206
+ first_response = subject.get(@path)
207
+ assert_equal 'cached data', first_response.body
208
+ assert_equal 'MISS', first_response.headers['X-Cache']
209
+
210
+ # Second request should use If-None-Match
211
+ cached_response = subject.get(@path)
212
+ assert_equal 'cached data', cached_response.body
213
+ assert_equal 'HIT', cached_response.headers['X-Cache']
214
+
215
+ mock.verify
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestClient < Minitest::Test
6
+ LOCAL_ONLY = Reforge::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
+
28
+
29
+ def test_get_with_missing_default
30
+ client = new_client
31
+ # it raises by default
32
+ err = assert_raises(Reforge::Errors::MissingDefaultError) do
33
+ assert_nil client.get('missing_value')
34
+ end
35
+
36
+ assert_match(/No value found for key/, err.message)
37
+ assert_match(/on_no_default/, err.message)
38
+
39
+ # you can opt-in to return `nil` instead
40
+ client = new_client(on_no_default: Reforge::Options::ON_NO_DEFAULT::RETURN_NIL)
41
+ assert_nil client.get('missing_value')
42
+ end
43
+
44
+
45
+
46
+
47
+
48
+ def test_initialization_with_an_options_object
49
+ options_hash = {
50
+ namespace: 'test-namespace',
51
+ prefab_datasources: LOCAL_ONLY
52
+ }
53
+
54
+ options = Reforge::Options.new(options_hash)
55
+
56
+ client = Reforge::Client.new(options)
57
+
58
+ assert_equal client.namespace, 'test-namespace'
59
+ end
60
+
61
+ def test_initialization_with_a_hash
62
+ options_hash = {
63
+ namespace: 'test-namespace',
64
+ prefab_datasources: LOCAL_ONLY
65
+ }
66
+
67
+ client = Reforge::Client.new(options_hash)
68
+
69
+ assert_equal client.namespace, 'test-namespace'
70
+ end
71
+
72
+ def test_evaluation_summary_aggregator
73
+ fake_api_key = '123-development-yourapikey-SDK'
74
+
75
+ # it is nil by default
76
+ assert_nil new_client(sdk_key: fake_api_key).evaluation_summary_aggregator
77
+
78
+ # it is nil when local_only even if collect_max_evaluation_summaries is true
79
+ assert_nil new_client(prefab_datasources: LOCAL_ONLY,
80
+ collect_evaluation_summaries: true, ).evaluation_summary_aggregator
81
+
82
+ # it is nil when collect_max_evaluation_summaries is false
83
+ assert_nil new_client(sdk_key: fake_api_key,
84
+ prefab_datasources: :all,
85
+ collect_evaluation_summaries: false).evaluation_summary_aggregator
86
+
87
+ # it is not nil when collect_max_evaluation_summaries is true and the datasource is not local_only
88
+ assert_equal Reforge::EvaluationSummaryAggregator,
89
+ new_client(sdk_key: fake_api_key,
90
+ prefab_datasources: :all,
91
+ collect_evaluation_summaries: true).evaluation_summary_aggregator.class
92
+
93
+ assert_logged [
94
+ "Reforge::ConfigClient -- No success loading checkpoints"
95
+ ]
96
+ end
97
+
98
+ def test_get_with_basic_value
99
+ config = basic_value_config
100
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID, collect_evaluation_summaries: true,
101
+ context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
102
+
103
+ assert_equal DESIRED_VALUE, client.get(config.key, IRRELEVANT, 'user' => { 'key' => 99 })
104
+
105
+ assert_summary client, {
106
+ [KEY, :CONFIG] => {
107
+ {
108
+ config_id: config.id,
109
+ config_row_index: 1,
110
+ selected_value: DESIRED_VALUE_CONFIG,
111
+ conditional_value_index: 0,
112
+ weighted_value_index: nil,
113
+ selected_index: nil
114
+ } => 1
115
+ }
116
+ }
117
+
118
+ assert_example_contexts client, [Reforge::Context.new({ user: { 'key' => 99 } })]
119
+ end
120
+
121
+ def test_get_with_basic_value_with_context
122
+ config = basic_value_config
123
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID, collect_evaluation_summaries: true,
124
+ context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
125
+
126
+ client.with_context('user' => { 'key' => 99 }) do
127
+ assert_equal DESIRED_VALUE, client.get(config.key)
128
+ end
129
+
130
+ assert_summary client, {
131
+ [KEY, :CONFIG] => {
132
+ {
133
+ config_id: config.id,
134
+ config_row_index: 1,
135
+ selected_value: DESIRED_VALUE_CONFIG,
136
+ conditional_value_index: 0,
137
+ weighted_value_index: nil,
138
+ selected_index: nil
139
+ } => 1
140
+ }
141
+ }
142
+
143
+ assert_example_contexts client, [Reforge::Context.new({ user: { 'key' => 99 } })]
144
+ end
145
+
146
+ def test_get_with_weighted_values
147
+ config = PrefabProto::Config.new(
148
+ id: 123,
149
+ key: KEY,
150
+ config_type: PrefabProto::ConfigType::CONFIG,
151
+ rows: [
152
+ DEFAULT_ROW,
153
+ PrefabProto::ConfigRow.new(
154
+ project_env_id: PROJECT_ENV_ID,
155
+ values: [
156
+ PrefabProto::ConditionalValue.new(
157
+ criteria: [PrefabProto::Criterion.new(operator: PrefabProto::Criterion::CriterionOperator::ALWAYS_TRUE)],
158
+ value: PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 98], ['def', 1],
159
+ ['ghi', 1]]))
160
+ )
161
+ ]
162
+ )
163
+ ]
164
+ )
165
+
166
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID, collect_evaluation_summaries: true,
167
+ context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
168
+
169
+ 2.times do
170
+ assert_equal 'abc', client.get(config.key, IRRELEVANT, 'user' => { 'key' => '1' })
171
+ end
172
+
173
+ 3.times do
174
+ assert_equal 'def', client.get(config.key, IRRELEVANT, 'user' => { 'key' => '12' })
175
+ end
176
+
177
+ assert_equal 'ghi',
178
+ client.get(config.key, IRRELEVANT, 'user' => { 'key' => '4', admin: true })
179
+
180
+ assert_summary client, {
181
+ [KEY, :CONFIG] => {
182
+ {
183
+ config_id: config.id,
184
+ config_row_index: 1,
185
+ selected_value: PrefabProto::ConfigValue.new(string: 'abc'),
186
+ conditional_value_index: 0,
187
+ weighted_value_index: 0,
188
+ selected_index: nil
189
+ } => 2,
190
+
191
+ {
192
+ config_id: config.id,
193
+ config_row_index: 1,
194
+ selected_value: PrefabProto::ConfigValue.new(string: 'def'),
195
+ conditional_value_index: 0,
196
+ weighted_value_index: 1,
197
+ selected_index: nil
198
+ } => 3,
199
+
200
+ {
201
+ config_id: config.id,
202
+ config_row_index: 1,
203
+ selected_value: PrefabProto::ConfigValue.new(string: 'ghi'),
204
+ conditional_value_index: 0,
205
+ weighted_value_index: 2,
206
+ selected_index: nil
207
+ } => 1
208
+ }
209
+ }
210
+
211
+ assert_example_contexts client, [
212
+ Reforge::Context.new(user: { 'key' => '1' }),
213
+ Reforge::Context.new(user: { 'key' => '12' }),
214
+ Reforge::Context.new(user: { 'key' => '4', admin: true })
215
+ ]
216
+ end
217
+
218
+ def test_in_seg
219
+ segment_key = 'segment_key'
220
+
221
+ segment_config = PrefabProto::Config.new(
222
+ config_type: PrefabProto::ConfigType::SEGMENT,
223
+ key: segment_key,
224
+ rows: [
225
+ PrefabProto::ConfigRow.new(
226
+ values: [
227
+ PrefabProto::ConditionalValue.new(
228
+ value: TRUE_CONFIG,
229
+ criteria: [
230
+ PrefabProto::Criterion.new(
231
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
232
+ value_to_match: string_list(['hotmail.com', 'gmail.com']),
233
+ property_name: 'user.email'
234
+ )
235
+ ]
236
+ ),
237
+ PrefabProto::ConditionalValue.new(value: FALSE_CONFIG)
238
+ ]
239
+ )
240
+ ]
241
+ )
242
+
243
+ config = PrefabProto::Config.new(
244
+ key: KEY,
245
+ rows: [
246
+ DEFAULT_ROW,
247
+
248
+ PrefabProto::ConfigRow.new(
249
+ project_env_id: PROJECT_ENV_ID,
250
+ values: [
251
+ PrefabProto::ConditionalValue.new(
252
+ criteria: [
253
+ PrefabProto::Criterion.new(
254
+ operator: PrefabProto::Criterion::CriterionOperator::IN_SEG,
255
+ value_to_match: PrefabProto::ConfigValue.new(string: segment_key)
256
+ )
257
+ ],
258
+ value: DESIRED_VALUE_CONFIG
259
+ )
260
+ ]
261
+ )
262
+ ]
263
+ )
264
+
265
+ client = new_client(config: [config, segment_config], project_env_id: PROJECT_ENV_ID,
266
+ collect_evaluation_summaries: true, context_upload_mode: :periodic_example, allow_telemetry_in_local_mode: true)
267
+
268
+ assert_equal DEFAULT_VALUE, client.get(config.key)
269
+ assert_equal DEFAULT_VALUE,
270
+ client.get(config.key, IRRELEVANT, user: { key: 'abc', email: 'example@prefab.cloud' })
271
+ assert_equal DESIRED_VALUE, client.get(config.key, IRRELEVANT, user: { key: 'def', email: 'example@hotmail.com' })
272
+
273
+ assert_summary client, {
274
+ [segment_key, :SEGMENT] => {
275
+ { config_id: 0, config_row_index: 0, conditional_value_index: 1, selected_value: FALSE_CONFIG,
276
+ weighted_value_index: nil, selected_index: nil } => 2,
277
+ { config_id: 0, config_row_index: 0, conditional_value_index: 0, selected_value: TRUE_CONFIG,
278
+ weighted_value_index: nil, selected_index: nil } => 1
279
+ },
280
+ [KEY, :NOT_SET_CONFIG_TYPE] => {
281
+ { config_id: 0, config_row_index: 0, conditional_value_index: 0, selected_value: DEFAULT_VALUE_CONFIG,
282
+ weighted_value_index: nil, selected_index: nil } => 2,
283
+ { config_id: 0, config_row_index: 1, conditional_value_index: 0, selected_value: DESIRED_VALUE_CONFIG,
284
+ weighted_value_index: nil, selected_index: nil } => 1
285
+ }
286
+ }
287
+
288
+ assert_example_contexts client, [
289
+ Reforge::Context.new(user: { key: 'abc', email: 'example@prefab.cloud' }),
290
+ Reforge::Context.new(user: { key: 'def', email: 'example@hotmail.com' })
291
+ ]
292
+ end
293
+
294
+ def test_get_log_level
295
+ config = PrefabProto::Config.new(
296
+ id: 999,
297
+ key: 'log-level',
298
+ config_type: PrefabProto::ConfigType::LOG_LEVEL,
299
+ rows: [
300
+ PrefabProto::ConfigRow.new(
301
+ values: [
302
+ PrefabProto::ConditionalValue.new(
303
+ criteria: [PrefabProto::Criterion.new(operator: PrefabProto::Criterion::CriterionOperator::ALWAYS_TRUE)],
304
+ value: PrefabProto::ConfigValue.new(log_level: PrefabProto::LogLevel::INFO)
305
+ )
306
+ ]
307
+ )
308
+ ]
309
+ )
310
+
311
+ client = new_client(config: config, project_env_id: PROJECT_ENV_ID,
312
+ collect_evaluation_summaries: true, allow_telemetry_in_local_mode: true)
313
+
314
+ assert_equal :INFO, client.get(config.key, IRRELEVANT)
315
+
316
+ # nothing is summarized for log levels
317
+ assert_summary client, {}
318
+ end
319
+
320
+
321
+
322
+ def test_with_datafile
323
+ datafile = "#{Dir.pwd}/test/fixtures/datafile.json"
324
+ client = new_client(datafile: datafile, prefab_datasources: :all)
325
+
326
+ assert client.get('flag.list.environments')
327
+ assert_equal "hello world", client.get('my.test.string')
328
+ end
329
+
330
+ private
331
+
332
+ def basic_value_config
333
+ PrefabProto::Config.new(
334
+ id: 123,
335
+ key: KEY,
336
+ config_type: PrefabProto::ConfigType::CONFIG,
337
+ rows: [
338
+ DEFAULT_ROW,
339
+ PrefabProto::ConfigRow.new(
340
+ project_env_id: PROJECT_ENV_ID,
341
+ values: [
342
+ PrefabProto::ConditionalValue.new(
343
+ criteria: [PrefabProto::Criterion.new(operator: PrefabProto::Criterion::CriterionOperator::ALWAYS_TRUE)],
344
+ value: DESIRED_VALUE_CONFIG
345
+ )
346
+ ]
347
+ )
348
+ ]
349
+ )
350
+ end
351
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigClient < Minitest::Test
6
+ def setup
7
+ super
8
+ options = Reforge::Options.new(
9
+ prefab_datasources: Reforge::Options::DATASOURCES::LOCAL_ONLY,
10
+ x_use_local_cache: true,
11
+ )
12
+
13
+ @config_client = Reforge::ConfigClient.new(MockBaseClient.new(options), 10)
14
+ end
15
+
16
+
17
+ def test_initialization_timeout_error
18
+ options = Reforge::Options.new(
19
+ sdk_key: '123-ENV-KEY-SDK',
20
+ initialization_timeout_sec: 0.01
21
+ )
22
+
23
+ err = assert_raises(Reforge::Errors::InitializationTimeoutError) do
24
+ Reforge::Client.new(options).config_client.get('anything')
25
+ end
26
+
27
+ assert_match(/couldn't initialize in 0.01 second timeout/, err.message)
28
+ end
29
+
30
+
31
+ def test_invalid_api_key_error
32
+ options = Reforge::Options.new(
33
+ sdk_key: ''
34
+ )
35
+
36
+ err = assert_raises(Reforge::Errors::InvalidSdkKeyError) do
37
+ Reforge::Client.new(options).config_client.get('anything')
38
+ end
39
+
40
+ assert_match(/No SDK key/, err.message)
41
+
42
+ options = Reforge::Options.new(
43
+ sdk_key: 'invalid'
44
+ )
45
+
46
+ err = assert_raises(Reforge::Errors::InvalidSdkKeyError) do
47
+ Reforge::Client.new(options).config_client.get('anything')
48
+ end
49
+
50
+ assert_match(/format is invalid/, err.message)
51
+ end
52
+
53
+ def test_caching
54
+ @config_client.send(:cache_configs,
55
+ PrefabProto::Configs.new(configs:
56
+ [PrefabProto::Config.new(key: 'test', id: 1,
57
+ rows: [PrefabProto::ConfigRow.new(
58
+ values: [
59
+ PrefabProto::ConditionalValue.new(
60
+ value: PrefabProto::ConfigValue.new(string: "test value")
61
+ )
62
+ ]
63
+ )])],
64
+ config_service_pointer: PrefabProto::ConfigServicePointer.new(project_id: 3, project_env_id: 5)))
65
+ @config_client.send(:load_cache)
66
+ assert_equal "test value", @config_client.get("test")
67
+ end
68
+
69
+ def test_cache_path_respects_xdg
70
+ options = Reforge::Options.new(
71
+ prefab_datasources: Reforge::Options::DATASOURCES::LOCAL_ONLY,
72
+ x_use_local_cache: true,
73
+ sdk_key: "123-ENV-KEY-SDK",)
74
+
75
+ config_client = Reforge::ConfigClient.new(MockBaseClient.new(options), 10)
76
+ assert_equal "#{Dir.home}/.cache/prefab.cache.123.json", config_client.send(:cache_path)
77
+
78
+ with_env('XDG_CACHE_HOME', '/tmp') do
79
+ config_client = Reforge::ConfigClient.new(MockBaseClient.new(options), 10)
80
+ assert_equal "/tmp/prefab.cache.123.json", config_client.send(:cache_path)
81
+ end
82
+ end
83
+
84
+ end