quonfig 0.0.10 → 0.0.12
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 +30 -0
- data/README.md +94 -0
- data/lib/quonfig/caching_http_connection.rb +3 -3
- data/lib/quonfig/client.rb +22 -27
- data/lib/quonfig/config_loader.rb +5 -1
- data/lib/quonfig/config_store.rb +10 -6
- data/lib/quonfig/context.rb +5 -4
- data/lib/quonfig/datadir.rb +4 -10
- data/lib/quonfig/dev_context.rb +4 -2
- data/lib/quonfig/duration.rb +2 -2
- data/lib/quonfig/encryption.rb +12 -16
- data/lib/quonfig/errors/invalid_environment_error.rb +1 -3
- data/lib/quonfig/errors/invalid_sdk_key_error.rb +6 -7
- data/lib/quonfig/errors/missing_env_var_error.rb +0 -3
- data/lib/quonfig/errors/missing_environment_error.rb +1 -1
- data/lib/quonfig/errors/uninitialized_error.rb +1 -1
- data/lib/quonfig/evaluation.rb +11 -8
- data/lib/quonfig/evaluator.rb +34 -37
- data/lib/quonfig/fixed_size_hash.rb +1 -0
- data/lib/quonfig/http_connection.rb +2 -4
- data/lib/quonfig/internal_logger.rb +63 -27
- data/lib/quonfig/murmer3.rb +2 -2
- data/lib/quonfig/options.rb +62 -75
- data/lib/quonfig/periodic_sync.rb +1 -1
- data/lib/quonfig/quonfig.rb +3 -3
- data/lib/quonfig/reason.rb +2 -1
- data/lib/quonfig/resolver.rb +8 -9
- data/lib/quonfig/semantic_logger_filter.rb +4 -3
- data/lib/quonfig/semver.rb +6 -8
- data/lib/quonfig/sse_config_client.rb +14 -15
- data/lib/quonfig/stdlib_formatter.rb +3 -3
- data/lib/quonfig/telemetry/context_shape_aggregator.rb +2 -3
- data/lib/quonfig/telemetry/example_contexts_aggregator.rb +1 -1
- data/lib/quonfig/telemetry/telemetry_reporter.rb +1 -0
- data/lib/quonfig/time_helpers.rb +2 -0
- data/lib/quonfig/version.rb +5 -0
- data/lib/quonfig.rb +2 -1
- data/quonfig.gemspec +29 -165
- metadata +24 -193
- 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_details_getters.rb +0 -242
- 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 -81
- data/test/test_internal_logger.rb +0 -34
- data/test/test_options.rb +0 -198
- 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,209 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestTelemetryReporter < Minitest::Test
|
|
6
|
-
# Minimal stand-in for Quonfig::HttpConnection that records POSTs.
|
|
7
|
-
class FakeHttpConnection
|
|
8
|
-
FakeResponse = Struct.new(:status)
|
|
9
|
-
|
|
10
|
-
attr_reader :posts
|
|
11
|
-
|
|
12
|
-
def initialize
|
|
13
|
-
@posts = []
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def post(path, body)
|
|
17
|
-
@posts << [path, body]
|
|
18
|
-
FakeResponse.new(200)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def make_options(telemetry_destination: 'https://telemetry.example.com',
|
|
23
|
-
sdk_key: 'qf_sk_development_abc_deadbeef')
|
|
24
|
-
# Build minimal Options bypassing env-var lookups via explicit overrides.
|
|
25
|
-
Quonfig::Options.new(
|
|
26
|
-
sdk_key: sdk_key,
|
|
27
|
-
environment: 'development',
|
|
28
|
-
api_urls: ['https://primary.example.com'],
|
|
29
|
-
enable_sse: false,
|
|
30
|
-
enable_polling: false,
|
|
31
|
-
on_init_failure: Quonfig::Options::ON_INITIALIZATION_FAILURE::RETURN,
|
|
32
|
-
context_upload_mode: :periodic_example
|
|
33
|
-
).tap do |opts|
|
|
34
|
-
opts.instance_variable_set(:@telemetry_destination, telemetry_destination)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def test_sync_posts_combined_events_in_api_telemetry_wire_shape
|
|
39
|
-
options = make_options
|
|
40
|
-
fake = FakeHttpConnection.new
|
|
41
|
-
|
|
42
|
-
shape_agg = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 100)
|
|
43
|
-
example_agg = Quonfig::Telemetry::ExampleContextsAggregator.new(max_contexts: 100)
|
|
44
|
-
|
|
45
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
46
|
-
options: options,
|
|
47
|
-
instance_hash: 'fake-instance-hash',
|
|
48
|
-
context_shape_aggregator: shape_agg,
|
|
49
|
-
example_contexts_aggregator: example_agg,
|
|
50
|
-
http_connection: fake
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
ctx = Quonfig::Context.new('user' => { 'key' => 'abc', 'age' => 33 })
|
|
54
|
-
shape_agg.push(ctx)
|
|
55
|
-
example_agg.record(ctx)
|
|
56
|
-
|
|
57
|
-
reporter.sync
|
|
58
|
-
|
|
59
|
-
assert_equal 1, fake.posts.size
|
|
60
|
-
path, body = fake.posts.first
|
|
61
|
-
assert_equal '/api/v1/telemetry/', path
|
|
62
|
-
|
|
63
|
-
# Wire shape: TelemetryEventsSchema
|
|
64
|
-
assert_equal 'fake-instance-hash', body['instanceHash']
|
|
65
|
-
assert_kind_of Array, body['events']
|
|
66
|
-
assert_equal 2, body['events'].size
|
|
67
|
-
|
|
68
|
-
shape_event = body['events'].find { |e| e.key?('contextShapes') }
|
|
69
|
-
example_event = body['events'].find { |e| e.key?('exampleContexts') }
|
|
70
|
-
|
|
71
|
-
refute_nil shape_event
|
|
72
|
-
refute_nil example_event
|
|
73
|
-
|
|
74
|
-
# ContextShapesSchema: { shapes: [{ name, fieldTypes }] }
|
|
75
|
-
shapes = shape_event['contextShapes']['shapes']
|
|
76
|
-
assert_equal 1, shapes.size
|
|
77
|
-
assert_equal 'user', shapes[0]['name']
|
|
78
|
-
assert_equal({ 'key' => 2, 'age' => 1 }, shapes[0]['fieldTypes'])
|
|
79
|
-
|
|
80
|
-
# ExampleContextsSchema: { examples: [{ timestamp, contextSet: { contexts: [...] } }] }
|
|
81
|
-
examples = example_event['exampleContexts']['examples']
|
|
82
|
-
assert_equal 1, examples.size
|
|
83
|
-
assert_kind_of Integer, examples[0]['timestamp']
|
|
84
|
-
contexts_list = examples[0]['contextSet']['contexts']
|
|
85
|
-
assert_equal 'user', contexts_list[0]['type']
|
|
86
|
-
assert_equal 'abc', contexts_list[0]['values']['key']
|
|
87
|
-
assert_equal 33, contexts_list[0]['values']['age']
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def test_sync_noop_when_aggregators_empty
|
|
91
|
-
fake = FakeHttpConnection.new
|
|
92
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
93
|
-
options: make_options,
|
|
94
|
-
instance_hash: 'h',
|
|
95
|
-
context_shape_aggregator: Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 10),
|
|
96
|
-
example_contexts_aggregator: Quonfig::Telemetry::ExampleContextsAggregator.new(max_contexts: 10),
|
|
97
|
-
http_connection: fake
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
reporter.sync
|
|
101
|
-
assert_equal 0, fake.posts.size
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def test_enabled_requires_sdk_key_and_destination
|
|
105
|
-
options = make_options(sdk_key: '')
|
|
106
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
107
|
-
options: options,
|
|
108
|
-
instance_hash: 'h',
|
|
109
|
-
context_shape_aggregator: Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 10)
|
|
110
|
-
)
|
|
111
|
-
refute reporter.enabled?
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def test_record_feeds_both_aggregators
|
|
115
|
-
fake = FakeHttpConnection.new
|
|
116
|
-
shape_agg = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 100)
|
|
117
|
-
example_agg = Quonfig::Telemetry::ExampleContextsAggregator.new(max_contexts: 100)
|
|
118
|
-
|
|
119
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
120
|
-
options: make_options,
|
|
121
|
-
instance_hash: 'h',
|
|
122
|
-
context_shape_aggregator: shape_agg,
|
|
123
|
-
example_contexts_aggregator: example_agg,
|
|
124
|
-
http_connection: fake
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
reporter.record(Quonfig::Context.new('user' => { 'key' => 'zzz' }))
|
|
128
|
-
|
|
129
|
-
refute_nil shape_agg.drain_event
|
|
130
|
-
refute_nil example_agg.drain_event
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def test_sync_posts_evaluation_summaries_event
|
|
134
|
-
fake = FakeHttpConnection.new
|
|
135
|
-
summaries_agg = Quonfig::Telemetry::EvaluationSummariesAggregator.new(max_keys: 100)
|
|
136
|
-
|
|
137
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
138
|
-
options: make_options,
|
|
139
|
-
instance_hash: 'h',
|
|
140
|
-
evaluation_summaries_aggregator: summaries_agg,
|
|
141
|
-
http_connection: fake
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
summaries_agg.record(
|
|
145
|
-
config_id: 'cid',
|
|
146
|
-
config_key: 'my-key',
|
|
147
|
-
config_type: 'config',
|
|
148
|
-
conditional_value_index: 0,
|
|
149
|
-
selected_value: 'v',
|
|
150
|
-
reason: 1
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
reporter.sync
|
|
154
|
-
|
|
155
|
-
assert_equal 1, fake.posts.size
|
|
156
|
-
_path, body = fake.posts.first
|
|
157
|
-
|
|
158
|
-
summaries_event = body['events'].find { |e| e.key?('summaries') }
|
|
159
|
-
refute_nil summaries_event, 'expected a summaries event in the payload'
|
|
160
|
-
|
|
161
|
-
inner = summaries_event['summaries']
|
|
162
|
-
assert_kind_of Array, inner['summaries']
|
|
163
|
-
counter = inner['summaries'][0]['counters'][0]
|
|
164
|
-
assert_equal 1, counter['count']
|
|
165
|
-
assert_equal 1, counter['reason']
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def test_at_exit_final_drain_posts_pending_batch
|
|
169
|
-
fake = FakeHttpConnection.new
|
|
170
|
-
shape_agg = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 100)
|
|
171
|
-
|
|
172
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
173
|
-
options: make_options,
|
|
174
|
-
instance_hash: 'h',
|
|
175
|
-
context_shape_aggregator: shape_agg,
|
|
176
|
-
http_connection: fake
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
# Simulate evaluations accumulating between sync cycles.
|
|
180
|
-
shape_agg.push(Quonfig::Context.new('user' => { 'key' => 'x' }))
|
|
181
|
-
|
|
182
|
-
# Simulate a Rails SIGTERM: process exits without Client#stop being
|
|
183
|
-
# called. The reporter's at_exit handler must flush the pending batch.
|
|
184
|
-
reporter.send(:final_drain_on_exit)
|
|
185
|
-
|
|
186
|
-
assert_equal 1, fake.posts.size
|
|
187
|
-
_path, body = fake.posts.first
|
|
188
|
-
refute_nil body['events'].find { |e| e.key?('contextShapes') }
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def test_at_exit_handler_registered_on_start
|
|
192
|
-
# Guards against regressing the Kernel.at_exit hookup that catches
|
|
193
|
-
# Rails / Passenger worker SIGTERMs where Client#stop isn't called.
|
|
194
|
-
fake = FakeHttpConnection.new
|
|
195
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
196
|
-
options: make_options,
|
|
197
|
-
instance_hash: 'h',
|
|
198
|
-
context_shape_aggregator: Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 10),
|
|
199
|
-
http_connection: fake,
|
|
200
|
-
sync_interval: 999 # avoid the background thread firing during the test
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
refute reporter.at_exit_registered?, 'not registered before start'
|
|
204
|
-
reporter.start
|
|
205
|
-
assert reporter.at_exit_registered?, 'registered after start'
|
|
206
|
-
ensure
|
|
207
|
-
reporter&.stop
|
|
208
|
-
end
|
|
209
|
-
end
|
data/test/test_typed_getters.rb
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
# Typed getters (get_string / get_int / get_bool / get_string_list /
|
|
6
|
-
# get_duration / get_json) on Quonfig::Client. Each verifies both the happy
|
|
7
|
-
# path against an injected ConfigStore and the type-mismatch error path.
|
|
8
|
-
class TestTypedGetters < Minitest::Test
|
|
9
|
-
KEY = 'my.key'
|
|
10
|
-
|
|
11
|
-
def make_config(value:, type:)
|
|
12
|
-
{
|
|
13
|
-
'id' => '1',
|
|
14
|
-
'key' => KEY,
|
|
15
|
-
'type' => 'config',
|
|
16
|
-
'valueType' => type,
|
|
17
|
-
'sendToClientSdk' => false,
|
|
18
|
-
'default' => {
|
|
19
|
-
'rules' => [
|
|
20
|
-
{
|
|
21
|
-
'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
22
|
-
'value' => { 'type' => type, 'value' => value }
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
},
|
|
26
|
-
'environment' => nil
|
|
27
|
-
}
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def client_with_value(value:, type:)
|
|
31
|
-
store = Quonfig::ConfigStore.new
|
|
32
|
-
store.set(KEY, make_config(value: value, type: type))
|
|
33
|
-
Quonfig::Client.new(Quonfig::Options.new, store: store)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# ---- get_string -------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
def test_get_string_returns_string
|
|
39
|
-
assert_equal 'hello', client_with_value(value: 'hello', type: 'string').get_string(KEY)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def test_get_string_raises_on_non_string
|
|
43
|
-
assert_raises(Quonfig::Errors::TypeMismatchError) do
|
|
44
|
-
client_with_value(value: 42, type: 'int').get_string(KEY)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def test_get_string_default_when_missing
|
|
49
|
-
client = Quonfig::Client.new(Quonfig::Options.new, store: Quonfig::ConfigStore.new)
|
|
50
|
-
assert_equal 'fallback', client.get_string('nope', default: 'fallback')
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# ---- get_int ----------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
def test_get_int_returns_integer
|
|
56
|
-
assert_equal 42, client_with_value(value: 42, type: 'int').get_int(KEY)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_get_int_raises_on_string
|
|
60
|
-
assert_raises(Quonfig::Errors::TypeMismatchError) do
|
|
61
|
-
client_with_value(value: 'oops', type: 'string').get_int(KEY)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# ---- get_float --------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
def test_get_float_returns_float
|
|
68
|
-
assert_in_delta 3.14, client_with_value(value: 3.14, type: 'double').get_float(KEY), 0.0001
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def test_get_float_raises_on_non_float
|
|
72
|
-
assert_raises(Quonfig::Errors::TypeMismatchError) do
|
|
73
|
-
client_with_value(value: 'oops', type: 'string').get_float(KEY)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# ---- get_bool ---------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
def test_get_bool_returns_true
|
|
80
|
-
assert_equal true, client_with_value(value: true, type: 'bool').get_bool(KEY)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def test_get_bool_returns_false
|
|
84
|
-
assert_equal false, client_with_value(value: false, type: 'bool').get_bool(KEY)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def test_get_bool_raises_on_string
|
|
88
|
-
assert_raises(Quonfig::Errors::TypeMismatchError) do
|
|
89
|
-
client_with_value(value: 'true', type: 'string').get_bool(KEY)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# ---- get_string_list --------------------------------------------------
|
|
94
|
-
|
|
95
|
-
def test_get_string_list_returns_array_of_strings
|
|
96
|
-
assert_equal %w[a b c], client_with_value(value: %w[a b c], type: 'string_list').get_string_list(KEY)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def test_get_string_list_raises_on_non_array
|
|
100
|
-
assert_raises(Quonfig::Errors::TypeMismatchError) do
|
|
101
|
-
client_with_value(value: 'a,b,c', type: 'string').get_string_list(KEY)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# ---- get_duration -----------------------------------------------------
|
|
106
|
-
|
|
107
|
-
def test_get_duration_returns_milliseconds_for_iso_string
|
|
108
|
-
# ISO-8601 PT1S -> 1 second -> 1000 ms.
|
|
109
|
-
client = client_with_value(value: 'PT1S', type: 'duration')
|
|
110
|
-
assert_equal 1000, client.get_duration(KEY)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def test_get_duration_passes_through_numeric
|
|
114
|
-
client = client_with_value(value: 5000, type: 'int')
|
|
115
|
-
assert_equal 5000, client.get_duration(KEY)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# ---- get_json ---------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
def test_get_json_returns_hash_unchanged
|
|
121
|
-
payload = { 'a' => 1, 'b' => [1, 2, 3] }
|
|
122
|
-
client = client_with_value(value: payload, type: 'json')
|
|
123
|
-
assert_equal payload, client.get_json(KEY)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def test_get_json_returns_array_unchanged
|
|
127
|
-
payload = [1, 2, 3]
|
|
128
|
-
client = client_with_value(value: payload, type: 'json')
|
|
129
|
-
assert_equal payload, client.get_json(KEY)
|
|
130
|
-
end
|
|
131
|
-
end
|
data/test/test_types.rb
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'test_helper'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'quonfig/types'
|
|
6
|
-
|
|
7
|
-
class TestTypes < Minitest::Test
|
|
8
|
-
def test_value_holds_type_and_value
|
|
9
|
-
v = Quonfig::Value.new(type: 'double', value: '9.95')
|
|
10
|
-
assert_equal 'double', v.type
|
|
11
|
-
assert_equal '9.95', v.value
|
|
12
|
-
assert_nil v.confidential
|
|
13
|
-
assert_nil v.decrypt_with
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_criterion_fields_accept_nil
|
|
17
|
-
c = Quonfig::Criterion.new(operator: 'ALWAYS_TRUE')
|
|
18
|
-
assert_equal 'ALWAYS_TRUE', c.operator
|
|
19
|
-
assert_nil c.property_name
|
|
20
|
-
assert_nil c.value_to_match
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def test_rule_wires_criteria_and_value
|
|
24
|
-
value = Quonfig::Value.new(type: 'double', value: '9.95')
|
|
25
|
-
criterion = Quonfig::Criterion.new(operator: 'ALWAYS_TRUE')
|
|
26
|
-
rule = Quonfig::Rule.new(criteria: [criterion], value: value)
|
|
27
|
-
assert_equal [criterion], rule.criteria
|
|
28
|
-
assert_same value, rule.value
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def test_rule_set_holds_rules_array
|
|
32
|
-
rs = Quonfig::RuleSet.new(rules: [])
|
|
33
|
-
assert_equal [], rs.rules
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def test_environment_holds_id_and_rules
|
|
37
|
-
env = Quonfig::Environment.new(id: 'env-1', rules: [])
|
|
38
|
-
assert_equal 'env-1', env.id
|
|
39
|
-
assert_equal [], env.rules
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def test_meta_optional_workspace_id
|
|
43
|
-
meta = Quonfig::Meta.new(version: '1', environment: 'production')
|
|
44
|
-
assert_nil meta.workspace_id
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def test_weighted_value_and_weighted_values_data
|
|
48
|
-
wv = Quonfig::WeightedValue.new(weight: 100, value: Quonfig::Value.new(type: 'bool', value: true))
|
|
49
|
-
assert_equal 100, wv.weight
|
|
50
|
-
wvd = Quonfig::WeightedValuesData.new(weighted_values: [wv])
|
|
51
|
-
assert_equal [wv], wvd.weighted_values
|
|
52
|
-
assert_nil wvd.hash_by_property_name
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def test_schema_data
|
|
56
|
-
sd = Quonfig::SchemaData.new(schema_type: 'zod', schema: 'z.string()')
|
|
57
|
-
assert_equal 'zod', sd.schema_type
|
|
58
|
-
assert_equal 'z.string()', sd.schema
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def test_provided_data
|
|
62
|
-
pd = Quonfig::ProvidedData.new(source: 'ENV_VAR', lookup: 'DATABASE_URL')
|
|
63
|
-
assert_equal 'ENV_VAR', pd.source
|
|
64
|
-
assert_equal 'DATABASE_URL', pd.lookup
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def test_config_response_from_integration_fixture
|
|
68
|
-
path = File.expand_path(
|
|
69
|
-
'../../integration-test-data/data/integration-tests/configs/my-double-key.json',
|
|
70
|
-
__dir__
|
|
71
|
-
)
|
|
72
|
-
skip "integration-test-data not present at #{path}" unless File.exist?(path)
|
|
73
|
-
|
|
74
|
-
raw = JSON.parse(File.read(path))
|
|
75
|
-
default_rules = raw.fetch('default').fetch('rules').map do |rule|
|
|
76
|
-
criteria = rule.fetch('criteria').map do |c|
|
|
77
|
-
Quonfig::Criterion.new(
|
|
78
|
-
property_name: c['propertyName'],
|
|
79
|
-
operator: c.fetch('operator'),
|
|
80
|
-
value_to_match: c['valueToMatch']
|
|
81
|
-
)
|
|
82
|
-
end
|
|
83
|
-
value = rule.fetch('value')
|
|
84
|
-
Quonfig::Rule.new(
|
|
85
|
-
criteria: criteria,
|
|
86
|
-
value: Quonfig::Value.new(type: value['type'], value: value['value'])
|
|
87
|
-
)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
response = Quonfig::ConfigResponse.new(
|
|
91
|
-
id: raw.fetch('id'),
|
|
92
|
-
key: raw.fetch('key'),
|
|
93
|
-
type: raw.fetch('type'),
|
|
94
|
-
value_type: raw.fetch('valueType'),
|
|
95
|
-
send_to_client_sdk: raw.fetch('sendToClientSdk'),
|
|
96
|
-
default: Quonfig::RuleSet.new(rules: default_rules)
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
assert_equal 'my-double-key', response.key
|
|
100
|
-
assert_equal 'config', response.type
|
|
101
|
-
assert_equal 'double', response.value_type
|
|
102
|
-
refute response.send_to_client_sdk
|
|
103
|
-
assert_nil response.environment
|
|
104
|
-
assert_equal 1, response.default.rules.length
|
|
105
|
-
|
|
106
|
-
rule = response.default.rules.first
|
|
107
|
-
assert_equal 'ALWAYS_TRUE', rule.criteria.first.operator
|
|
108
|
-
assert_equal 'double', rule.value.type
|
|
109
|
-
assert_equal '9.95', rule.value.value
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def test_config_envelope_carries_configs_and_meta
|
|
113
|
-
meta = Quonfig::Meta.new(version: '1', environment: 'production')
|
|
114
|
-
envelope = Quonfig::ConfigEnvelope.new(configs: [], meta: meta)
|
|
115
|
-
assert_equal [], envelope.configs
|
|
116
|
-
assert_same meta, envelope.meta
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def test_workspace_config_document_fields
|
|
120
|
-
doc = Quonfig::WorkspaceConfigDocument.new(
|
|
121
|
-
id: 'id-1',
|
|
122
|
-
key: 'feature.x',
|
|
123
|
-
type: 'feature_flag',
|
|
124
|
-
value_type: 'bool',
|
|
125
|
-
send_to_client_sdk: true,
|
|
126
|
-
default: Quonfig::RuleSet.new(rules: []),
|
|
127
|
-
environments: []
|
|
128
|
-
)
|
|
129
|
-
assert_equal 'feature.x', doc.key
|
|
130
|
-
assert_equal 'feature_flag', doc.type
|
|
131
|
-
assert_equal 'bool', doc.value_type
|
|
132
|
-
assert doc.send_to_client_sdk
|
|
133
|
-
assert_equal [], doc.environments
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def test_workspace_environment_fields
|
|
137
|
-
we = Quonfig::WorkspaceEnvironment.new(id: 'env-1', rules: [])
|
|
138
|
-
assert_equal 'env-1', we.id
|
|
139
|
-
assert_equal [], we.rules
|
|
140
|
-
end
|
|
141
|
-
end
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestWeightedValueResolver < Minitest::Test
|
|
6
|
-
KEY = 'config_key'
|
|
7
|
-
|
|
8
|
-
def test_resolving_single_value
|
|
9
|
-
values = weighted_values([['abc', 1]])
|
|
10
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, nil)
|
|
11
|
-
assert_equal 'abc', resolver.resolve[0][:value]
|
|
12
|
-
assert_equal 0, resolver.resolve[1]
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def test_resolving_multiple_values_evenly_distributed
|
|
16
|
-
values = weighted_values([['abc', 1], ['def', 1]])
|
|
17
|
-
|
|
18
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:001')
|
|
19
|
-
assert_equal 'abc', resolver.resolve[0][:value]
|
|
20
|
-
assert_equal 0, resolver.resolve[1]
|
|
21
|
-
|
|
22
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:456')
|
|
23
|
-
assert_equal 'def', resolver.resolve[0][:value]
|
|
24
|
-
assert_equal 1, resolver.resolve[1]
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def test_resolving_multiple_values_unevenly_distributed
|
|
28
|
-
values = weighted_values([['abc', 1], ['def', 98], ['ghi', 1]])
|
|
29
|
-
|
|
30
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:456')
|
|
31
|
-
assert_equal 'def', resolver.resolve[0][:value]
|
|
32
|
-
assert_equal 1, resolver.resolve[1]
|
|
33
|
-
|
|
34
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:103')
|
|
35
|
-
assert_equal 'ghi', resolver.resolve[0][:value]
|
|
36
|
-
assert_equal 2, resolver.resolve[1]
|
|
37
|
-
|
|
38
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:119')
|
|
39
|
-
assert_equal 'abc', resolver.resolve[0][:value]
|
|
40
|
-
assert_equal 0, resolver.resolve[1]
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def test_resolving_multiple_values_with_simulation
|
|
44
|
-
values = weighted_values([['abc', 1], ['def', 98], ['ghi', 1]])
|
|
45
|
-
results = {}
|
|
46
|
-
|
|
47
|
-
10_000.times do |i|
|
|
48
|
-
result = Quonfig::WeightedValueResolver.new(values, KEY, "user:#{i}").resolve[0][:value]
|
|
49
|
-
results[result] ||= 0
|
|
50
|
-
results[result] += 1
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
assert_in_delta 100, results['abc'], 20
|
|
54
|
-
assert_in_delta 9800, results['def'], 50
|
|
55
|
-
assert_in_delta 100, results['ghi'], 20
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def test_string_keyed_weighted_value_hashes
|
|
59
|
-
# JSON.parse without symbolize_names yields string keys; the resolver must
|
|
60
|
-
# accept that shape too.
|
|
61
|
-
values = [
|
|
62
|
-
{ 'value' => 'abc', 'weight' => 1 },
|
|
63
|
-
{ 'value' => 'def', 'weight' => 1 }
|
|
64
|
-
]
|
|
65
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:456')
|
|
66
|
-
assert_equal 'def', resolver.resolve[0]['value']
|
|
67
|
-
assert_equal 1, resolver.resolve[1]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def test_all_zero_weights_returns_last_variant
|
|
71
|
-
values = weighted_values([['abc', 0], ['def', 0], ['ghi', 0]])
|
|
72
|
-
resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:001')
|
|
73
|
-
assert_equal 'ghi', resolver.resolve[0][:value]
|
|
74
|
-
assert_equal 2, resolver.resolve[1]
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
def weighted_values(values_and_weights)
|
|
80
|
-
values_and_weights.map do |value, weight|
|
|
81
|
-
{ value: value, weight: weight }
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|