quonfig 0.0.9 → 0.0.11
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 +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +4 -4
- data/lib/quonfig/evaluation_details.rb +60 -0
- data/lib/quonfig/options.rb +37 -16
- data/lib/quonfig/sse_config_client.rb +1 -1
- data/lib/quonfig/version.rb +5 -0
- data/lib/quonfig.rb +2 -1
- data/quonfig.gemspec +30 -163
- metadata +29 -182
- data/.claude/rules/constitution.md +0 -81
- data/.claude/rules/git-safety.md +0 -11
- data/.claude/rules/issue-tracking.md +0 -13
- data/.claude/rules/testing-workflow.md +0 -28
- data/.envrc.sample +0 -3
- data/.github/CODEOWNERS +0 -2
- data/.github/pull_request_template.md +0 -8
- data/.github/workflows/release.yml +0 -49
- data/.github/workflows/ruby.yml +0 -60
- data/.github/workflows/test.yaml +0 -40
- data/.rubocop.yml +0 -13
- data/.tool-versions +0 -1
- data/CLAUDE.md +0 -29
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -26
- data/Gemfile.lock +0 -177
- data/Rakefile +0 -64
- data/VERSION +0 -1
- data/dev/allocation_stats +0 -60
- data/dev/benchmark +0 -40
- data/dev/console +0 -12
- data/dev/script_setup.rb +0 -18
- data/test/fixtures/datafile.json +0 -87
- data/test/integration/test_context_precedence.rb +0 -112
- data/test/integration/test_datadir_environment.rb +0 -54
- data/test/integration/test_dev_overrides.rb +0 -40
- data/test/integration/test_enabled.rb +0 -478
- data/test/integration/test_enabled_with_contexts.rb +0 -64
- data/test/integration/test_get.rb +0 -136
- data/test/integration/test_get_feature_flag.rb +0 -28
- data/test/integration/test_get_or_raise.rb +0 -60
- data/test/integration/test_get_weighted_values.rb +0 -34
- data/test/integration/test_helpers.rb +0 -667
- data/test/integration/test_helpers_test.rb +0 -73
- data/test/integration/test_post.rb +0 -44
- data/test/integration/test_telemetry.rb +0 -170
- data/test/support/common_helpers.rb +0 -106
- data/test/support/mock_base_client.rb +0 -27
- data/test/support/mock_config_loader.rb +0 -1
- data/test/test_bound_client.rb +0 -109
- data/test/test_caching_http_connection.rb +0 -218
- data/test/test_client.rb +0 -255
- data/test/test_client_network_mode.rb +0 -136
- data/test/test_client_telemetry.rb +0 -175
- data/test/test_config_loader.rb +0 -70
- data/test/test_context.rb +0 -139
- data/test/test_context_shape.rb +0 -37
- data/test/test_context_shape_aggregator.rb +0 -126
- data/test/test_datadir.rb +0 -203
- data/test/test_dev_context.rb +0 -163
- data/test/test_duration.rb +0 -37
- data/test/test_encryption.rb +0 -16
- data/test/test_evaluation_summaries_aggregator.rb +0 -180
- data/test/test_evaluator.rb +0 -285
- data/test/test_example_contexts_aggregator.rb +0 -119
- data/test/test_exponential_backoff.rb +0 -44
- data/test/test_fixed_size_hash.rb +0 -119
- data/test/test_helper.rb +0 -17
- data/test/test_http_connection.rb +0 -79
- data/test/test_internal_logger.rb +0 -34
- data/test/test_options.rb +0 -167
- data/test/test_rate_limit_cache.rb +0 -44
- data/test/test_reason.rb +0 -79
- data/test/test_rename.rb +0 -65
- data/test/test_resolver.rb +0 -291
- data/test/test_semantic_logger_filter.rb +0 -144
- data/test/test_semver.rb +0 -108
- data/test/test_should_log.rb +0 -186
- data/test/test_sse_config_client.rb +0 -297
- data/test/test_stdlib_formatter.rb +0 -195
- data/test/test_telemetry_reporter.rb +0 -209
- data/test/test_typed_getters.rb +0 -131
- data/test/test_types.rb +0 -141
- data/test/test_weighted_value_resolver.rb +0 -84
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
module Quonfig
|
|
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
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
# Quonfig::Client wires the JSON stack: ConfigStore + Evaluator + Resolver
|
|
6
|
-
# (introduced in qfg-dk6.4-9). These tests drive Client through an injected
|
|
7
|
-
# ConfigStore so they never touch the network or the filesystem. The legacy
|
|
8
|
-
# protobuf ConfigClient/ConfigResolver path was removed in qfg-dk6.32.
|
|
9
|
-
class TestClient < Minitest::Test
|
|
10
|
-
CONFIG_KEY = 'my.flag'
|
|
11
|
-
|
|
12
|
-
# ---- Test fixtures -----------------------------------------------------
|
|
13
|
-
|
|
14
|
-
# Plain ConfigResponse-shaped hash (mirrors what
|
|
15
|
-
# Quonfig::Datadir.to_config_response and IntegrationTestHelpers emit).
|
|
16
|
-
def make_config(key:, value:, type: 'string', criteria: nil)
|
|
17
|
-
{
|
|
18
|
-
'id' => '1',
|
|
19
|
-
'key' => key,
|
|
20
|
-
'type' => 'config',
|
|
21
|
-
'valueType' => type,
|
|
22
|
-
'sendToClientSdk' => false,
|
|
23
|
-
'default' => {
|
|
24
|
-
'rules' => [
|
|
25
|
-
{
|
|
26
|
-
'criteria' => criteria || [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
27
|
-
'value' => { 'type' => type, 'value' => value }
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
},
|
|
31
|
-
'environment' => nil
|
|
32
|
-
}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def store_with(*configs)
|
|
36
|
-
store = Quonfig::ConfigStore.new
|
|
37
|
-
configs.each { |c| store.set(c['key'], c) }
|
|
38
|
-
store
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def client_with(store, **options)
|
|
42
|
-
Quonfig::Client.new(Quonfig::Options.new(**options), store: store)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# ---- Construction ------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
def test_constructor_accepts_options_object
|
|
48
|
-
client = client_with(Quonfig::ConfigStore.new)
|
|
49
|
-
assert_kind_of Quonfig::Options, client.options
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def test_constructor_wires_resolver_and_evaluator
|
|
53
|
-
store = Quonfig::ConfigStore.new
|
|
54
|
-
client = Quonfig::Client.new(Quonfig::Options.new, store: store)
|
|
55
|
-
|
|
56
|
-
assert_kind_of Quonfig::Resolver, client.resolver
|
|
57
|
-
assert_kind_of Quonfig::Evaluator, client.evaluator
|
|
58
|
-
assert_same store, client.store,
|
|
59
|
-
'Client must use the injected ConfigStore instance'
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def test_instance_hash_is_unique_per_client
|
|
63
|
-
a = client_with(Quonfig::ConfigStore.new)
|
|
64
|
-
b = client_with(Quonfig::ConfigStore.new)
|
|
65
|
-
refute_equal a.instance_hash, b.instance_hash
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# ---- get returns coerced JSON values, not PrefabProto ------------------
|
|
69
|
-
|
|
70
|
-
def test_get_returns_string_value
|
|
71
|
-
store = store_with(make_config(key: CONFIG_KEY, value: 'hello'))
|
|
72
|
-
assert_equal 'hello', client_with(store).get(CONFIG_KEY)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def test_get_returns_int_value
|
|
76
|
-
store = store_with(make_config(key: CONFIG_KEY, value: 42, type: 'int'))
|
|
77
|
-
assert_equal 42, client_with(store).get(CONFIG_KEY)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def test_get_returns_bool_value
|
|
81
|
-
store = store_with(make_config(key: CONFIG_KEY, value: true, type: 'bool'))
|
|
82
|
-
assert_equal true, client_with(store).get(CONFIG_KEY)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def test_get_returned_value_is_not_a_prefab_proto
|
|
86
|
-
store = store_with(make_config(key: CONFIG_KEY, value: 'hello'))
|
|
87
|
-
value = client_with(store).get(CONFIG_KEY)
|
|
88
|
-
|
|
89
|
-
refute value.respond_to?(:string_list),
|
|
90
|
-
'Client#get must return a plain Ruby value, not a PrefabProto::ConfigValue'
|
|
91
|
-
refute value.is_a?(Hash),
|
|
92
|
-
'Client#get must unwrap to the coerced Ruby value, not the JSON Value hash'
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# ---- Missing key handling ---------------------------------------------
|
|
96
|
-
|
|
97
|
-
def test_get_returns_explicit_default_when_key_missing
|
|
98
|
-
store = Quonfig::ConfigStore.new
|
|
99
|
-
assert_equal 'fallback', client_with(store).get('nope', 'fallback')
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def test_get_raises_missing_default_error_by_default
|
|
103
|
-
store = Quonfig::ConfigStore.new
|
|
104
|
-
assert_raises(Quonfig::Errors::MissingDefaultError) do
|
|
105
|
-
client_with(store).get('nope')
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def test_get_returns_nil_when_on_no_default_is_return_nil
|
|
110
|
-
store = Quonfig::ConfigStore.new
|
|
111
|
-
client = client_with(store, on_no_default: Quonfig::Options::ON_NO_DEFAULT::RETURN_NIL)
|
|
112
|
-
assert_nil client.get('nope')
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# ---- enabled? --------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
def test_enabled_returns_true_when_value_is_true
|
|
118
|
-
store = store_with(make_config(key: CONFIG_KEY, value: true, type: 'bool'))
|
|
119
|
-
assert client_with(store).enabled?(CONFIG_KEY)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def test_enabled_returns_false_when_value_is_false
|
|
123
|
-
store = store_with(make_config(key: CONFIG_KEY, value: false, type: 'bool'))
|
|
124
|
-
refute client_with(store).enabled?(CONFIG_KEY)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def test_enabled_returns_false_for_missing_key
|
|
128
|
-
store = Quonfig::ConfigStore.new
|
|
129
|
-
refute client_with(store).enabled?('nope')
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# ---- defined? + keys --------------------------------------------------
|
|
133
|
-
|
|
134
|
-
def test_defined_returns_true_for_known_key
|
|
135
|
-
store = store_with(make_config(key: CONFIG_KEY, value: 'x'))
|
|
136
|
-
assert client_with(store).defined?(CONFIG_KEY)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def test_defined_returns_false_for_unknown_key
|
|
140
|
-
store = store_with(make_config(key: CONFIG_KEY, value: 'x'))
|
|
141
|
-
refute client_with(store).defined?('absent')
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def test_keys_returns_store_keys
|
|
145
|
-
store = store_with(
|
|
146
|
-
make_config(key: 'a', value: '1'),
|
|
147
|
-
make_config(key: 'b', value: '2')
|
|
148
|
-
)
|
|
149
|
-
assert_equal %w[a b].sort, client_with(store).keys.sort
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# ---- Context: jit context is plain Hash, not PrefabProto::Context ----
|
|
153
|
-
|
|
154
|
-
def test_get_accepts_jit_context_as_plain_hash
|
|
155
|
-
cfg = make_config(
|
|
156
|
-
key: CONFIG_KEY,
|
|
157
|
-
value: 'matched',
|
|
158
|
-
criteria: [{
|
|
159
|
-
'operator' => 'PROP_IS_ONE_OF',
|
|
160
|
-
'propertyName' => 'user.role',
|
|
161
|
-
'valueToMatch' => { 'type' => 'string_list', 'value' => ['admin'] }
|
|
162
|
-
}]
|
|
163
|
-
)
|
|
164
|
-
store = store_with(cfg)
|
|
165
|
-
|
|
166
|
-
result = client_with(store).get(CONFIG_KEY, 'fallback', user: { 'role' => 'admin' })
|
|
167
|
-
|
|
168
|
-
assert_equal 'matched', result
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def test_with_context_returns_bound_client
|
|
172
|
-
bound = client_with(Quonfig::ConfigStore.new).with_context(user: { 'key' => '1' })
|
|
173
|
-
assert_kind_of Quonfig::BoundClient, bound
|
|
174
|
-
assert_equal({ user: { 'key' => '1' } }, bound.context)
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def test_in_context_yields_bound_client_when_block_given
|
|
178
|
-
yielded = nil
|
|
179
|
-
client_with(Quonfig::ConfigStore.new).in_context(user: { 'key' => '1' }) do |bound|
|
|
180
|
-
yielded = bound
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
assert_kind_of Quonfig::BoundClient, yielded
|
|
184
|
-
assert_equal({ user: { 'key' => '1' } }, yielded.context)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def test_global_context_is_merged_into_jit_context
|
|
188
|
-
cfg = make_config(
|
|
189
|
-
key: CONFIG_KEY,
|
|
190
|
-
value: 'admin-value',
|
|
191
|
-
criteria: [{
|
|
192
|
-
'operator' => 'PROP_IS_ONE_OF',
|
|
193
|
-
'propertyName' => 'user.role',
|
|
194
|
-
'valueToMatch' => { 'type' => 'string_list', 'value' => ['admin'] }
|
|
195
|
-
}]
|
|
196
|
-
)
|
|
197
|
-
store = store_with(cfg)
|
|
198
|
-
client = Quonfig::Client.new(
|
|
199
|
-
Quonfig::Options.new(global_context: { user: { 'role' => 'admin' } }),
|
|
200
|
-
store: store
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
assert_equal 'admin-value', client.get(CONFIG_KEY, 'fallback')
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def test_jit_context_overrides_global_context_at_the_property_level
|
|
207
|
-
cfg = make_config(
|
|
208
|
-
key: CONFIG_KEY,
|
|
209
|
-
value: 'jit-value',
|
|
210
|
-
criteria: [{
|
|
211
|
-
'operator' => 'PROP_IS_ONE_OF',
|
|
212
|
-
'propertyName' => 'user.role',
|
|
213
|
-
'valueToMatch' => { 'type' => 'string_list', 'value' => ['user'] }
|
|
214
|
-
}]
|
|
215
|
-
)
|
|
216
|
-
store = store_with(cfg)
|
|
217
|
-
client = Quonfig::Client.new(
|
|
218
|
-
Quonfig::Options.new(global_context: { user: { 'role' => 'admin' } }),
|
|
219
|
-
store: store
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# jit overrides global for this single property; keys unique to global preserved
|
|
223
|
-
assert_equal 'jit-value', client.get(CONFIG_KEY, 'fallback', user: { 'role' => 'user' })
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def test_normalize_context_rejects_non_hash_jit_context
|
|
227
|
-
store = Quonfig::ConfigStore.new
|
|
228
|
-
assert_raises(ArgumentError) do
|
|
229
|
-
client_with(store).get('nope', 'fallback', 'not-a-hash')
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# ---- Misc -------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
def test_stop_is_a_noop
|
|
236
|
-
client_with(Quonfig::ConfigStore.new).stop
|
|
237
|
-
pass
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def test_inspect_includes_environment
|
|
241
|
-
client = client_with(Quonfig::ConfigStore.new, environment: 'Production')
|
|
242
|
-
assert_match(/environment="Production"/, client.inspect)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def test_no_prefab_proto_in_lib_quonfig_source
|
|
246
|
-
# qfg-dk6.32: scrub PrefabProto from the runtime lib path.
|
|
247
|
-
lib_dir = File.expand_path('../lib/quonfig', __dir__)
|
|
248
|
-
offenders = Dir.glob(File.join(lib_dir, '**/*.rb')).select do |path|
|
|
249
|
-
File.read(path).match?(/PrefabProto/)
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
assert_empty offenders,
|
|
253
|
-
"lib/quonfig still references PrefabProto:\n#{offenders.join("\n")}"
|
|
254
|
-
end
|
|
255
|
-
end
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
require 'webrick'
|
|
5
|
-
require 'json'
|
|
6
|
-
|
|
7
|
-
# Verifies Client#initialize (qfg-s7h) wires HTTP fetch + ConfigStore together
|
|
8
|
-
# so `Quonfig.get(...)` / `Quonfig.enabled?(...)` return real values — not the
|
|
9
|
-
# defaults — when only an `sdk_key:` + `api_urls:` are supplied. This is the
|
|
10
|
-
# regression test for the P0 documented in test-ruby/FRICTION.md where
|
|
11
|
-
# network-mode was accepted but silently ignored in v0.0.3.
|
|
12
|
-
class TestClientNetworkMode < Minitest::Test
|
|
13
|
-
PORT = 18_094
|
|
14
|
-
|
|
15
|
-
SAMPLE_CONFIG = {
|
|
16
|
-
'id' => 'c1',
|
|
17
|
-
'key' => 'log-levels.test-ruby',
|
|
18
|
-
'type' => 'log_level',
|
|
19
|
-
'valueType' => 'log_level',
|
|
20
|
-
'sendToClientSdk' => false,
|
|
21
|
-
'default' => {
|
|
22
|
-
'rules' => [
|
|
23
|
-
{
|
|
24
|
-
'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
25
|
-
'value' => { 'type' => 'log_level', 'value' => 'WARN' }
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
}.freeze
|
|
30
|
-
|
|
31
|
-
def setup
|
|
32
|
-
super
|
|
33
|
-
@server = nil
|
|
34
|
-
@fetch_count = 0
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def teardown
|
|
38
|
-
@server&.shutdown
|
|
39
|
-
super
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def start_server
|
|
43
|
-
log = WEBrick::Log.new(StringIO.new)
|
|
44
|
-
@server = WEBrick::HTTPServer.new(
|
|
45
|
-
Port: PORT, Logger: log, AccessLog: []
|
|
46
|
-
)
|
|
47
|
-
@server.mount_proc '/api/v2/configs' do |_req, res|
|
|
48
|
-
@fetch_count += 1
|
|
49
|
-
res.status = 200
|
|
50
|
-
res['Content-Type'] = 'application/json'
|
|
51
|
-
res['ETag'] = "v#{@fetch_count}"
|
|
52
|
-
res.body = JSON.generate(
|
|
53
|
-
'configs' => [SAMPLE_CONFIG],
|
|
54
|
-
'meta' => { 'version' => "v#{@fetch_count}", 'environment' => 'dev' }
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
Thread.new { @server.start }
|
|
58
|
-
# Wait for server to be ready.
|
|
59
|
-
50.times do
|
|
60
|
-
break if tcp_open?
|
|
61
|
-
sleep 0.05
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def tcp_open?
|
|
66
|
-
require 'socket'
|
|
67
|
-
TCPSocket.new('127.0.0.1', PORT).tap(&:close)
|
|
68
|
-
true
|
|
69
|
-
rescue StandardError
|
|
70
|
-
false
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def test_initialize_fetches_configs_from_api_urls_and_populates_store
|
|
74
|
-
start_server
|
|
75
|
-
|
|
76
|
-
client = Quonfig::Client.new(
|
|
77
|
-
sdk_key: 'test-key',
|
|
78
|
-
api_urls: ["http://127.0.0.1:#{PORT}"],
|
|
79
|
-
enable_sse: false,
|
|
80
|
-
enable_polling: false
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
assert_equal 1, @fetch_count, 'expected exactly one HTTP fetch during init'
|
|
84
|
-
assert_includes client.keys, 'log-levels.test-ruby'
|
|
85
|
-
assert_equal 'WARN', client.get('log-levels.test-ruby', 'default')
|
|
86
|
-
ensure
|
|
87
|
-
client&.stop
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def test_initialize_raises_on_fetch_failure_by_default
|
|
91
|
-
# No server started -> connection refused everywhere
|
|
92
|
-
assert_raises(RuntimeError, Quonfig::Errors::InitializationTimeoutError) do
|
|
93
|
-
Quonfig::Client.new(
|
|
94
|
-
sdk_key: 'test-key',
|
|
95
|
-
api_urls: ['http://127.0.0.1:1'], # almost certainly unreachable
|
|
96
|
-
enable_sse: false,
|
|
97
|
-
enable_polling: false,
|
|
98
|
-
initialization_timeout_sec: 2
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def test_initialize_returns_empty_store_when_on_init_failure_is_return
|
|
104
|
-
client = Quonfig::Client.new(
|
|
105
|
-
sdk_key: 'test-key',
|
|
106
|
-
api_urls: ['http://127.0.0.1:1'],
|
|
107
|
-
enable_sse: false,
|
|
108
|
-
enable_polling: false,
|
|
109
|
-
initialization_timeout_sec: 2,
|
|
110
|
-
on_init_failure: Quonfig::Options::ON_INITIALIZATION_FAILURE::RETURN
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
assert_empty client.keys
|
|
114
|
-
assert_logged [/Initialization did not complete cleanly/]
|
|
115
|
-
ensure
|
|
116
|
-
client&.stop
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def test_initialize_skips_network_when_store_injected
|
|
120
|
-
# store: passed -> Client should not try any I/O. Unreachable URL must
|
|
121
|
-
# be fine when a store is injected.
|
|
122
|
-
store = Quonfig::ConfigStore.new
|
|
123
|
-
client = Quonfig::Client.new(
|
|
124
|
-
Quonfig::Options.new(
|
|
125
|
-
sdk_key: 'test-key',
|
|
126
|
-
api_urls: ['http://127.0.0.1:1'],
|
|
127
|
-
enable_sse: false,
|
|
128
|
-
enable_polling: false
|
|
129
|
-
),
|
|
130
|
-
store: store
|
|
131
|
-
)
|
|
132
|
-
assert_same store, client.store
|
|
133
|
-
ensure
|
|
134
|
-
client&.stop
|
|
135
|
-
end
|
|
136
|
-
end
|