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.
- checksums.yaml +7 -0
- data/.envrc.sample +3 -0
- data/.github/CODEOWNERS +2 -0
- data/.github/pull_request_template.md +8 -0
- data/.github/workflows/ruby.yml +48 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +257 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +182 -0
- data/LICENSE.txt +20 -0
- data/README.md +105 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/compile_protos.sh +20 -0
- data/dev/allocation_stats +60 -0
- data/dev/benchmark +40 -0
- data/dev/console +12 -0
- data/dev/script_setup.rb +18 -0
- data/lib/prefab_pb.rb +77 -0
- data/lib/reforge/caching_http_connection.rb +95 -0
- data/lib/reforge/client.rb +133 -0
- data/lib/reforge/config_client.rb +275 -0
- data/lib/reforge/config_client_presenter.rb +18 -0
- data/lib/reforge/config_loader.rb +67 -0
- data/lib/reforge/config_resolver.rb +84 -0
- data/lib/reforge/config_value_unwrapper.rb +123 -0
- data/lib/reforge/config_value_wrapper.rb +18 -0
- data/lib/reforge/context.rb +241 -0
- data/lib/reforge/context_shape.rb +20 -0
- data/lib/reforge/context_shape_aggregator.rb +70 -0
- data/lib/reforge/criteria_evaluator.rb +345 -0
- data/lib/reforge/duration.rb +58 -0
- data/lib/reforge/encryption.rb +65 -0
- data/lib/reforge/error.rb +6 -0
- data/lib/reforge/errors/env_var_parse_error.rb +11 -0
- data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
- data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
- data/lib/reforge/errors/missing_default_error.rb +13 -0
- data/lib/reforge/errors/missing_env_var_error.rb +11 -0
- data/lib/reforge/errors/uninitialized_error.rb +13 -0
- data/lib/reforge/evaluation.rb +53 -0
- data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
- data/lib/reforge/example_contexts_aggregator.rb +77 -0
- data/lib/reforge/exponential_backoff.rb +21 -0
- data/lib/reforge/feature_flag_client.rb +43 -0
- data/lib/reforge/fixed_size_hash.rb +14 -0
- data/lib/reforge/http_connection.rb +45 -0
- data/lib/reforge/internal_logger.rb +43 -0
- data/lib/reforge/javascript_stub.rb +99 -0
- data/lib/reforge/local_config_parser.rb +151 -0
- data/lib/reforge/murmer3.rb +50 -0
- data/lib/reforge/options.rb +191 -0
- data/lib/reforge/periodic_sync.rb +74 -0
- data/lib/reforge/prefab.rb +120 -0
- data/lib/reforge/rate_limit_cache.rb +41 -0
- data/lib/reforge/resolved_config_presenter.rb +86 -0
- data/lib/reforge/semver.rb +132 -0
- data/lib/reforge/sse_config_client.rb +112 -0
- data/lib/reforge/time_helpers.rb +7 -0
- data/lib/reforge/weighted_value_resolver.rb +42 -0
- data/lib/reforge/yaml_config_parser.rb +34 -0
- data/lib/reforge-sdk.rb +57 -0
- data/test/fixtures/datafile.json +87 -0
- data/test/integration_test.rb +171 -0
- data/test/integration_test_helpers.rb +114 -0
- data/test/support/common_helpers.rb +201 -0
- data/test/support/mock_base_client.rb +41 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_caching_http_connection.rb +218 -0
- data/test/test_client.rb +351 -0
- data/test/test_config_client.rb +84 -0
- data/test/test_config_loader.rb +82 -0
- data/test/test_config_resolver.rb +502 -0
- data/test/test_config_value_unwrapper.rb +270 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +271 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +150 -0
- data/test/test_criteria_evaluator.rb +1180 -0
- data/test/test_duration.rb +37 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +233 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +16 -0
- data/test/test_fixed_size_hash.rb +119 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +75 -0
- data/test/test_internal_logger.rb +25 -0
- data/test/test_javascript_stub.rb +176 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +93 -0
- data/test/test_prefab.rb +16 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_semver.rb +108 -0
- data/test/test_sse_config_client.rb +211 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- 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
|
data/test/test_client.rb
ADDED
@@ -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
|