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,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestInternalLogger < Minitest::Test
|
|
6
|
-
|
|
7
|
-
def teardown
|
|
8
|
-
# using_quonfig_log_filter! mutates the shared @@instances list — restore
|
|
9
|
-
# the default :warn level so it doesn't bleed into other tests' $logs.
|
|
10
|
-
Quonfig::InternalLogger.class_variable_get(:@@instances).each do |logger|
|
|
11
|
-
logger.level = :warn
|
|
12
|
-
end
|
|
13
|
-
super
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_levels
|
|
17
|
-
logger_a = Quonfig::InternalLogger.new(A)
|
|
18
|
-
logger_b = Quonfig::InternalLogger.new(B)
|
|
19
|
-
|
|
20
|
-
assert_equal :warn, logger_a.level
|
|
21
|
-
assert_equal :warn, logger_b.level
|
|
22
|
-
|
|
23
|
-
Quonfig::InternalLogger.using_quonfig_log_filter!
|
|
24
|
-
assert_equal :trace, logger_a.level
|
|
25
|
-
assert_equal :trace, logger_b.level
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
class A
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
class B
|
|
34
|
-
end
|
data/test/test_options.rb
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestOptions < Minitest::Test
|
|
6
|
-
API_KEY = 'abcdefg'
|
|
7
|
-
|
|
8
|
-
def test_api_urls_override_env_var
|
|
9
|
-
assert_equal Quonfig::Options::DEFAULT_API_URLS, Quonfig::Options.new.api_urls
|
|
10
|
-
|
|
11
|
-
# blank doesn't take effect
|
|
12
|
-
with_env('QUONFIG_API_URLS', '') do
|
|
13
|
-
assert_equal Quonfig::Options::DEFAULT_API_URLS, Quonfig::Options.new.api_urls
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# non-blank does take effect
|
|
17
|
-
with_env('QUONFIG_API_URLS', 'https://override.example.com') do
|
|
18
|
-
assert_equal ["https://override.example.com"], Quonfig::Options.new.api_urls
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def test_default_api_urls_point_to_quonfig
|
|
23
|
-
assert_equal [
|
|
24
|
-
'https://primary.quonfig.com',
|
|
25
|
-
], Quonfig::Options::DEFAULT_API_URLS
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def test_overriding_api_urls
|
|
29
|
-
assert_equal Quonfig::Options::DEFAULT_API_URLS, Quonfig::Options.new.api_urls
|
|
30
|
-
|
|
31
|
-
# a plain string ends up wrapped in an array
|
|
32
|
-
api_url = 'https://example.com'
|
|
33
|
-
assert_equal [api_url], Quonfig::Options.new(api_urls: api_url).api_urls
|
|
34
|
-
|
|
35
|
-
api_urls = ['https://example.com', 'https://example2.com']
|
|
36
|
-
assert_equal api_urls, Quonfig::Options.new(api_urls: api_urls).api_urls
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def test_derive_stream_url_prepends_stream_to_hostname
|
|
40
|
-
assert_equal 'https://stream.primary.quonfig.com',
|
|
41
|
-
Quonfig::Options.derive_stream_url('https://primary.quonfig.com')
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def test_derive_stream_url_preserves_port
|
|
45
|
-
assert_equal 'http://stream.localhost:6550',
|
|
46
|
-
Quonfig::Options.derive_stream_url('http://localhost:6550')
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def test_derive_stream_url_preserves_scheme_and_path
|
|
50
|
-
assert_equal 'http://stream.api.example.com/base',
|
|
51
|
-
Quonfig::Options.derive_stream_url('http://api.example.com/base')
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def test_derive_stream_url_with_eu_subdomain
|
|
55
|
-
assert_equal 'https://stream.primary.eu.quonfig.com',
|
|
56
|
-
Quonfig::Options.derive_stream_url('https://primary.eu.quonfig.com')
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_works_with_named_arguments
|
|
60
|
-
assert_equal API_KEY, Quonfig::Options.new(sdk_key: API_KEY).sdk_key
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def test_works_with_hash
|
|
64
|
-
assert_equal API_KEY, Quonfig::Options.new({ sdk_key: API_KEY }).sdk_key
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def test_sdk_key_reads_from_quonfig_backend_sdk_key
|
|
68
|
-
with_env('QUONFIG_BACKEND_SDK_KEY', 'env-key') do
|
|
69
|
-
assert_equal 'env-key', Quonfig::Options.new.sdk_key
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def test_environment_reads_from_quonfig_environment
|
|
74
|
-
with_env('QUONFIG_ENVIRONMENT', 'staging') do
|
|
75
|
-
assert_equal 'staging', Quonfig::Options.new.environment
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def test_environment_explicit_overrides_env_var
|
|
80
|
-
with_env('QUONFIG_ENVIRONMENT', 'staging') do
|
|
81
|
-
assert_equal 'production', Quonfig::Options.new(environment: 'production').environment
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def test_enable_sse_defaults_true
|
|
86
|
-
assert_equal true, Quonfig::Options.new.enable_sse
|
|
87
|
-
assert_equal false, Quonfig::Options.new(enable_sse: false).enable_sse
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def test_enable_polling_defaults_true
|
|
91
|
-
assert_equal true, Quonfig::Options.new.enable_polling
|
|
92
|
-
assert_equal false, Quonfig::Options.new(enable_polling: false).enable_polling
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def test_datadir_reads_from_quonfig_dir_env
|
|
96
|
-
with_env('QUONFIG_DIR', '/tmp/some/workspace') do
|
|
97
|
-
assert_equal '/tmp/some/workspace', Quonfig::Options.new.datadir
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def test_datadir_explicit_overrides_env_var
|
|
102
|
-
with_env('QUONFIG_DIR', '/tmp/env/workspace') do
|
|
103
|
-
assert_equal '/tmp/explicit', Quonfig::Options.new(datadir: '/tmp/explicit').datadir
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def test_datadir_predicate
|
|
108
|
-
assert_equal false, Quonfig::Options.new.datadir?
|
|
109
|
-
assert_equal true, Quonfig::Options.new(datadir: '/tmp/ws').datadir?
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def test_local_only_uses_datadir_presence
|
|
113
|
-
refute Quonfig::Options.new.local_only?
|
|
114
|
-
assert Quonfig::Options.new(datadir: '/tmp/ws').local_only?
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def test_collect_max_paths
|
|
118
|
-
assert_equal 1000, Quonfig::Options.new.collect_max_paths
|
|
119
|
-
assert_equal 100, Quonfig::Options.new(collect_max_paths: 100).collect_max_paths
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def test_collect_max_evaluation_summaries
|
|
123
|
-
assert_equal 100_000, Quonfig::Options.new.collect_max_evaluation_summaries
|
|
124
|
-
assert_equal 0, Quonfig::Options.new(collect_evaluation_summaries: false).collect_max_evaluation_summaries
|
|
125
|
-
assert_equal 3,
|
|
126
|
-
Quonfig::Options.new(collect_max_evaluation_summaries: 3).collect_max_evaluation_summaries
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def test_context_upload_mode_periodic
|
|
130
|
-
options = Quonfig::Options.new(context_upload_mode: :periodic_example, context_max_size: 100)
|
|
131
|
-
assert_equal 100, options.collect_max_example_contexts
|
|
132
|
-
|
|
133
|
-
options = Quonfig::Options.new(context_upload_mode: :none)
|
|
134
|
-
assert_equal 0, options.collect_max_example_contexts
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def test_context_upload_mode_shapes_only
|
|
138
|
-
options = Quonfig::Options.new(context_upload_mode: :shapes_only, context_max_size: 100)
|
|
139
|
-
assert_equal 100, options.collect_max_shapes
|
|
140
|
-
|
|
141
|
-
options = Quonfig::Options.new(context_upload_mode: :none)
|
|
142
|
-
assert_equal 0, options.collect_max_shapes
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def test_context_upload_mode_none
|
|
146
|
-
options = Quonfig::Options.new(context_upload_mode: :none)
|
|
147
|
-
assert_equal 0, options.collect_max_example_contexts
|
|
148
|
-
|
|
149
|
-
options = Quonfig::Options.new(context_upload_mode: :none)
|
|
150
|
-
assert_equal 0, options.collect_max_shapes
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def test_telemetry_destination_reads_env_first
|
|
154
|
-
with_env('QUONFIG_TELEMETRY_URL', 'https://custom-telemetry.example.com') do
|
|
155
|
-
assert_equal 'https://custom-telemetry.example.com', Quonfig::Options.new.telemetry_destination
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def test_telemetry_destination_derives_from_default_sources
|
|
160
|
-
assert_equal 'https://telemetry.quonfig.com', Quonfig::Options.new.telemetry_destination
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def test_telemetry_destination_derives_from_custom_quonfig_api_urls
|
|
164
|
-
options = Quonfig::Options.new(api_urls: ['https://primary.eu.quonfig.com'])
|
|
165
|
-
assert_equal 'https://telemetry.eu.quonfig.com', options.telemetry_destination
|
|
166
|
-
end
|
|
167
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
require 'timecop'
|
|
5
|
-
|
|
6
|
-
class RateLimitCacheTest < Minitest::Test
|
|
7
|
-
def test_set_and_fresh
|
|
8
|
-
cache = Quonfig::RateLimitCache.new(5)
|
|
9
|
-
cache.set('key')
|
|
10
|
-
assert cache.fresh?('key')
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def test_fresh_with_no_set
|
|
14
|
-
cache = Quonfig::RateLimitCache.new(5)
|
|
15
|
-
refute cache.fresh?('key')
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def test_get_after_expiration
|
|
19
|
-
cache = Quonfig::RateLimitCache.new(5)
|
|
20
|
-
|
|
21
|
-
Timecop.freeze(Time.now - 6) do
|
|
22
|
-
cache.set('key')
|
|
23
|
-
assert cache.fresh?('key')
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
refute cache.fresh?('key')
|
|
27
|
-
|
|
28
|
-
# but the data is still there
|
|
29
|
-
assert cache.data.get('key')
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def test_prune
|
|
33
|
-
cache = Quonfig::RateLimitCache.new(5)
|
|
34
|
-
|
|
35
|
-
Timecop.freeze(Time.now - 6) do
|
|
36
|
-
cache.set('key')
|
|
37
|
-
assert cache.fresh?('key')
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
cache.prune
|
|
41
|
-
|
|
42
|
-
refute cache.fresh?('key')
|
|
43
|
-
end
|
|
44
|
-
end
|
data/test/test_reason.rb
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'minitest/autorun'
|
|
4
|
-
require 'quonfig/reason'
|
|
5
|
-
|
|
6
|
-
# Tests Quonfig::Reason in isolation — uses plain Structs so the test does not
|
|
7
|
-
# depend on PrefabProto (which is not loaded by the current bootstrap).
|
|
8
|
-
class TestReason < Minitest::Test
|
|
9
|
-
FakeCriterion = Struct.new(:operator)
|
|
10
|
-
FakeConditionalValue = Struct.new(:criteria)
|
|
11
|
-
FakeRow = Struct.new(:values, :project_env_id)
|
|
12
|
-
FakeConfig = Struct.new(:rows)
|
|
13
|
-
|
|
14
|
-
ALWAYS_TRUE = FakeCriterion.new(:ALWAYS_TRUE)
|
|
15
|
-
PROP_MATCH = FakeCriterion.new(:PROP_IS_ONE_OF)
|
|
16
|
-
|
|
17
|
-
DEFAULT_CV = FakeConditionalValue.new([])
|
|
18
|
-
ALWAYS_TRUE_CV = FakeConditionalValue.new([ALWAYS_TRUE])
|
|
19
|
-
RULE_CV = FakeConditionalValue.new([PROP_MATCH])
|
|
20
|
-
|
|
21
|
-
def default_only_config
|
|
22
|
-
FakeConfig.new([FakeRow.new([DEFAULT_CV], 0)])
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def targeted_config
|
|
26
|
-
FakeConfig.new([
|
|
27
|
-
FakeRow.new([DEFAULT_CV], 0),
|
|
28
|
-
FakeRow.new([RULE_CV, ALWAYS_TRUE_CV], 1)
|
|
29
|
-
])
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def test_default_for_default_only_config
|
|
33
|
-
reason = Quonfig::Reason.compute(
|
|
34
|
-
config: default_only_config,
|
|
35
|
-
conditional_value: DEFAULT_CV
|
|
36
|
-
)
|
|
37
|
-
assert_equal :DEFAULT, reason
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def test_rule_match_when_targeting_rule_matched
|
|
41
|
-
reason = Quonfig::Reason.compute(
|
|
42
|
-
config: targeted_config,
|
|
43
|
-
conditional_value: RULE_CV
|
|
44
|
-
)
|
|
45
|
-
assert_equal :RULE_MATCH, reason
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def test_rule_match_when_falling_back_to_always_true_in_targeted_config
|
|
49
|
-
reason = Quonfig::Reason.compute(
|
|
50
|
-
config: targeted_config,
|
|
51
|
-
conditional_value: ALWAYS_TRUE_CV
|
|
52
|
-
)
|
|
53
|
-
assert_equal :RULE_MATCH, reason
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def test_split_when_weighted_value_index_positive
|
|
57
|
-
reason = Quonfig::Reason.compute(
|
|
58
|
-
config: default_only_config,
|
|
59
|
-
conditional_value: DEFAULT_CV,
|
|
60
|
-
weighted_value_index: 2
|
|
61
|
-
)
|
|
62
|
-
assert_equal :SPLIT, reason
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def test_weighted_value_index_zero_is_not_split
|
|
66
|
-
reason = Quonfig::Reason.compute(
|
|
67
|
-
config: default_only_config,
|
|
68
|
-
conditional_value: DEFAULT_CV,
|
|
69
|
-
weighted_value_index: 0
|
|
70
|
-
)
|
|
71
|
-
assert_equal :DEFAULT, reason
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def test_default_when_only_always_true_criteria_and_no_targeting_rules
|
|
75
|
-
config = FakeConfig.new([FakeRow.new([ALWAYS_TRUE_CV], 0)])
|
|
76
|
-
reason = Quonfig::Reason.compute(config: config, conditional_value: ALWAYS_TRUE_CV)
|
|
77
|
-
assert_equal :DEFAULT, reason
|
|
78
|
-
end
|
|
79
|
-
end
|
data/test/test_rename.rb
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestRename < Minitest::Test
|
|
6
|
-
def test_quonfig_module_is_defined
|
|
7
|
-
assert defined?(Quonfig), 'Quonfig module must be defined after rename'
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def test_reforge_module_is_gone
|
|
11
|
-
# NOTE: 'Reforge' intentionally not renamed — this test guards against the old constant leaking back in.
|
|
12
|
-
refute Object.const_defined?(:Reforge), 'Reforge constant must be removed after rename'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def test_quonfig_options_exists
|
|
16
|
-
assert defined?(Quonfig::Options), 'Quonfig::Options must be defined'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def test_quonfig_client_exists
|
|
20
|
-
assert defined?(Quonfig::Client), 'Quonfig::Client must be defined'
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def test_quonfig_sdk_key_env_var
|
|
24
|
-
# NOTE: the REFORGE_/PREFAB_ keys below are intentionally spelled out in string literals
|
|
25
|
-
# so the bulk rename tool does not touch them — we are asserting that the NEW env var name
|
|
26
|
-
# is the one the SDK reads.
|
|
27
|
-
old_key_a = 'REFORGE_' + 'BACKEND_SDK_KEY'
|
|
28
|
-
old_key_b = 'PREFAB_' + 'API_KEY'
|
|
29
|
-
original = ENV.to_h.slice('QUONFIG_BACKEND_SDK_KEY', old_key_a, old_key_b)
|
|
30
|
-
ENV.delete(old_key_a)
|
|
31
|
-
ENV.delete(old_key_b)
|
|
32
|
-
ENV['QUONFIG_BACKEND_SDK_KEY'] = 'quonfig-test-key-123'
|
|
33
|
-
options = Quonfig::Options.new
|
|
34
|
-
assert_equal 'quonfig-test-key-123', options.sdk_key
|
|
35
|
-
ensure
|
|
36
|
-
ENV.delete('QUONFIG_BACKEND_SDK_KEY')
|
|
37
|
-
original&.each { |k, v| ENV[k] = v }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def test_gemspec_file_is_quonfig
|
|
41
|
-
root = File.expand_path('..', __dir__)
|
|
42
|
-
assert File.exist?(File.join(root, 'quonfig.gemspec')),
|
|
43
|
-
'quonfig.gemspec must exist at repo root'
|
|
44
|
-
refute File.exist?(File.join(root, 'sdk-reforge.gemspec')),
|
|
45
|
-
'sdk-reforge.gemspec must be removed'
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def test_gemspec_name_is_quonfig
|
|
49
|
-
root = File.expand_path('..', __dir__)
|
|
50
|
-
spec = Gem::Specification.load(File.join(root, 'quonfig.gemspec'))
|
|
51
|
-
assert_equal 'quonfig', spec.name
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def test_lib_entrypoint_renamed
|
|
55
|
-
root = File.expand_path('..', __dir__)
|
|
56
|
-
assert File.exist?(File.join(root, 'lib', 'quonfig.rb')),
|
|
57
|
-
'lib/quonfig.rb must exist'
|
|
58
|
-
assert Dir.exist?(File.join(root, 'lib', 'quonfig')),
|
|
59
|
-
'lib/quonfig/ directory must exist'
|
|
60
|
-
refute File.exist?(File.join(root, 'lib', 'sdk-reforge.rb')),
|
|
61
|
-
'lib/sdk-reforge.rb must be removed'
|
|
62
|
-
refute Dir.exist?(File.join(root, 'lib', 'reforge')),
|
|
63
|
-
'lib/reforge/ directory must be removed'
|
|
64
|
-
end
|
|
65
|
-
end
|
data/test/test_resolver.rb
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
# Tests for the new public API trio introduced by qfg-dk6.9:
|
|
6
|
-
# store = Quonfig::ConfigStore.new(configs_hash)
|
|
7
|
-
# evaluator = Quonfig::Evaluator.new(store)
|
|
8
|
-
# resolver = Quonfig::Resolver.new(store, evaluator)
|
|
9
|
-
# result = resolver.get('my.flag', context)
|
|
10
|
-
#
|
|
11
|
-
# Mirrors the sdk-node pattern so the integration test suite (qfg-dk6.22-24)
|
|
12
|
-
# can construct these directly without a full Client.
|
|
13
|
-
#
|
|
14
|
-
# We deliberately do NOT use PrefabProto — the protobuf gem was dropped in
|
|
15
|
-
# qfg-dk6.4 and JSON Criterion types land in qfg-dk6.5 / operators port in
|
|
16
|
-
# qfg-dk6.10. These tests use minimal Struct doubles that satisfy the
|
|
17
|
-
# duck-typed shape the current CriteriaEvaluator reads.
|
|
18
|
-
class TestResolverTrio < Minitest::Test
|
|
19
|
-
CONFIG_KEY = 'my.flag'
|
|
20
|
-
DEFAULT_VALUE = 'default_value'
|
|
21
|
-
|
|
22
|
-
# qfg-dk6.10 — configs are now plain ConfigResponse-shaped hashes (symbol
|
|
23
|
-
# top-level keys + string keys inside rules/criteria). Matches what
|
|
24
|
-
# Quonfig::Datadir.to_config_response and
|
|
25
|
-
# IntegrationTestHelpers.to_config_response emit.
|
|
26
|
-
def make_default_config(key: CONFIG_KEY, value: DEFAULT_VALUE)
|
|
27
|
-
{
|
|
28
|
-
id: '1',
|
|
29
|
-
key: key,
|
|
30
|
-
type: 'config',
|
|
31
|
-
value_type: 'string',
|
|
32
|
-
send_to_client_sdk: false,
|
|
33
|
-
default: {
|
|
34
|
-
'rules' => [
|
|
35
|
-
{
|
|
36
|
-
'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
37
|
-
'value' => { 'type' => 'string', 'value' => value }
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
},
|
|
41
|
-
environment: nil
|
|
42
|
-
}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def base_client
|
|
46
|
-
MockBaseClient.new(Quonfig::Options.new)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# ---- ConfigStore ------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
def test_config_store_constructs_with_hash
|
|
52
|
-
cfg = make_default_config
|
|
53
|
-
store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
|
|
54
|
-
|
|
55
|
-
assert_equal cfg, store.get(CONFIG_KEY)
|
|
56
|
-
assert_equal [CONFIG_KEY], store.keys
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_config_store_constructs_empty
|
|
60
|
-
store = Quonfig::ConfigStore.new
|
|
61
|
-
assert_nil store.get('missing')
|
|
62
|
-
assert_empty store.keys
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def test_config_store_set_and_get
|
|
66
|
-
store = Quonfig::ConfigStore.new
|
|
67
|
-
cfg = make_default_config
|
|
68
|
-
store.set(CONFIG_KEY, cfg)
|
|
69
|
-
assert_equal cfg, store.get(CONFIG_KEY)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def test_config_store_all_configs_returns_hash
|
|
73
|
-
cfg = make_default_config
|
|
74
|
-
store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
|
|
75
|
-
|
|
76
|
-
all = store.all_configs
|
|
77
|
-
assert_kind_of Hash, all
|
|
78
|
-
assert_equal cfg, all[CONFIG_KEY]
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def test_config_store_all_configs_is_a_copy
|
|
82
|
-
cfg = make_default_config
|
|
83
|
-
store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
|
|
84
|
-
store.all_configs['mutated'] = :nope
|
|
85
|
-
assert_nil store.get('mutated')
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def test_config_store_clear_empties_the_store
|
|
89
|
-
store = Quonfig::ConfigStore.new({ CONFIG_KEY => make_default_config })
|
|
90
|
-
store.clear
|
|
91
|
-
assert_nil store.get(CONFIG_KEY)
|
|
92
|
-
assert_empty store.keys
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# ---- Evaluator --------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
def test_evaluator_accepts_store
|
|
98
|
-
store = Quonfig::ConfigStore.new
|
|
99
|
-
Quonfig::Evaluator.new(store, base_client: base_client)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# ---- Resolver ---------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
def test_resolver_raw_returns_config_from_store
|
|
105
|
-
cfg = make_default_config
|
|
106
|
-
store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
|
|
107
|
-
evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
|
|
108
|
-
resolver = Quonfig::Resolver.new(store, evaluator)
|
|
109
|
-
|
|
110
|
-
assert_equal cfg, resolver.raw(CONFIG_KEY)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def test_resolver_raw_returns_nil_for_missing_key
|
|
114
|
-
store = Quonfig::ConfigStore.new
|
|
115
|
-
evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
|
|
116
|
-
resolver = Quonfig::Resolver.new(store, evaluator)
|
|
117
|
-
|
|
118
|
-
assert_nil resolver.raw('nope')
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def test_resolver_get_returns_evaluation_for_default_row_with_empty_criteria
|
|
122
|
-
cfg = make_default_config
|
|
123
|
-
store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
|
|
124
|
-
evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
|
|
125
|
-
resolver = Quonfig::Resolver.new(store, evaluator)
|
|
126
|
-
|
|
127
|
-
result = resolver.get(CONFIG_KEY, Quonfig::Context.new({}))
|
|
128
|
-
|
|
129
|
-
refute_nil result
|
|
130
|
-
assert_kind_of Quonfig::EvalResult, result
|
|
131
|
-
# The EvalResult exposes both the raw JSON value hash (#value) and the
|
|
132
|
-
# coerced Ruby value (#unwrapped_value). Prefer unwrapped_value for
|
|
133
|
-
# assertions — it mirrors what the real Client returns.
|
|
134
|
-
assert_equal DEFAULT_VALUE, result.unwrapped_value
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def test_resolver_get_raises_missing_default_for_missing_key
|
|
138
|
-
store = Quonfig::ConfigStore.new
|
|
139
|
-
evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
|
|
140
|
-
resolver = Quonfig::Resolver.new(store, evaluator)
|
|
141
|
-
|
|
142
|
-
# Resolver.get raises Quonfig::Errors::MissingDefaultError when no
|
|
143
|
-
# config exists for the key (qfg-9x7 alignment with the shared YAML
|
|
144
|
-
# get_or_raise.yaml suite). Client.get catches this and folds it into
|
|
145
|
-
# the on_no_default policy / caller-supplied default.
|
|
146
|
-
assert_raises(Quonfig::Errors::MissingDefaultError) do
|
|
147
|
-
resolver.get('nope', Quonfig::Context.new({}))
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# ---- ENV_VAR provided value resolution (qfg-08q) ---------------------
|
|
152
|
-
|
|
153
|
-
# Build a config whose value comes from a `provided` ENV_VAR lookup.
|
|
154
|
-
# value_type drives coercion of the env var string back to the SDK type
|
|
155
|
-
# (mirrors sdk-node/sdk-go behavior).
|
|
156
|
-
def make_provided_config(key:, value_type:, lookup:)
|
|
157
|
-
{
|
|
158
|
-
id: '1',
|
|
159
|
-
key: key,
|
|
160
|
-
type: 'config',
|
|
161
|
-
value_type: value_type,
|
|
162
|
-
send_to_client_sdk: false,
|
|
163
|
-
default: {
|
|
164
|
-
'rules' => [
|
|
165
|
-
{
|
|
166
|
-
'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
167
|
-
'value' => {
|
|
168
|
-
'type' => 'provided',
|
|
169
|
-
'value' => { 'source' => 'ENV_VAR', 'lookup' => lookup }
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
]
|
|
173
|
-
},
|
|
174
|
-
environment: nil
|
|
175
|
-
}
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def with_env(name, value)
|
|
179
|
-
original = ENV[name]
|
|
180
|
-
ENV[name] = value
|
|
181
|
-
yield
|
|
182
|
-
ensure
|
|
183
|
-
if original.nil?
|
|
184
|
-
ENV.delete(name)
|
|
185
|
-
else
|
|
186
|
-
ENV[name] = original
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def build_resolver(cfg)
|
|
191
|
-
store = Quonfig::ConfigStore.new({ cfg[:key] => cfg })
|
|
192
|
-
evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
|
|
193
|
-
Quonfig::Resolver.new(store, evaluator)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def test_resolver_get_resolves_provided_env_var_as_string
|
|
197
|
-
cfg = make_provided_config(key: 'a.string', value_type: 'string', lookup: 'QFG_TEST_STRING')
|
|
198
|
-
resolver = build_resolver(cfg)
|
|
199
|
-
|
|
200
|
-
with_env('QFG_TEST_STRING', 'hello') do
|
|
201
|
-
result = resolver.get('a.string', Quonfig::Context.new({}))
|
|
202
|
-
assert_equal 'hello', result.unwrapped_value
|
|
203
|
-
assert_equal 'string', result.value_type
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def test_resolver_get_resolves_provided_env_var_as_int
|
|
208
|
-
cfg = make_provided_config(key: 'a.number', value_type: 'int', lookup: 'QFG_TEST_INT')
|
|
209
|
-
resolver = build_resolver(cfg)
|
|
210
|
-
|
|
211
|
-
with_env('QFG_TEST_INT', '1234') do
|
|
212
|
-
result = resolver.get('a.number', Quonfig::Context.new({}))
|
|
213
|
-
assert_equal 1234, result.unwrapped_value
|
|
214
|
-
assert_equal 'int', result.value_type
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def test_resolver_get_resolves_provided_env_var_as_double
|
|
219
|
-
cfg = make_provided_config(key: 'a.double', value_type: 'double', lookup: 'QFG_TEST_DOUBLE')
|
|
220
|
-
resolver = build_resolver(cfg)
|
|
221
|
-
|
|
222
|
-
with_env('QFG_TEST_DOUBLE', '3.14') do
|
|
223
|
-
result = resolver.get('a.double', Quonfig::Context.new({}))
|
|
224
|
-
assert_in_delta 3.14, result.unwrapped_value, 0.0001
|
|
225
|
-
assert_equal 'double', result.value_type
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def test_resolver_get_resolves_provided_env_var_as_bool
|
|
230
|
-
cfg = make_provided_config(key: 'a.bool', value_type: 'bool', lookup: 'QFG_TEST_BOOL')
|
|
231
|
-
resolver = build_resolver(cfg)
|
|
232
|
-
|
|
233
|
-
with_env('QFG_TEST_BOOL', 'true') do
|
|
234
|
-
result = resolver.get('a.bool', Quonfig::Context.new({}))
|
|
235
|
-
assert_equal true, result.unwrapped_value
|
|
236
|
-
assert_equal 'bool', result.value_type
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
with_env('QFG_TEST_BOOL', 'no') do
|
|
240
|
-
result = resolver.get('a.bool', Quonfig::Context.new({}))
|
|
241
|
-
assert_equal false, result.unwrapped_value
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def test_resolver_get_resolves_provided_env_var_as_string_list
|
|
246
|
-
cfg = make_provided_config(key: 'a.list', value_type: 'string_list', lookup: 'QFG_TEST_LIST')
|
|
247
|
-
resolver = build_resolver(cfg)
|
|
248
|
-
|
|
249
|
-
with_env('QFG_TEST_LIST', 'a, b ,c') do
|
|
250
|
-
result = resolver.get('a.list', Quonfig::Context.new({}))
|
|
251
|
-
assert_equal %w[a b c], result.unwrapped_value
|
|
252
|
-
assert_equal 'string_list', result.value_type
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def test_resolver_get_raises_missing_env_var_error_when_unset
|
|
257
|
-
cfg = make_provided_config(key: 'a.missing', value_type: 'string', lookup: 'QFG_DEFINITELY_UNSET')
|
|
258
|
-
resolver = build_resolver(cfg)
|
|
259
|
-
ENV.delete('QFG_DEFINITELY_UNSET')
|
|
260
|
-
|
|
261
|
-
err = assert_raises(Quonfig::Errors::MissingEnvVarError) do
|
|
262
|
-
resolver.get('a.missing', Quonfig::Context.new({}))
|
|
263
|
-
end
|
|
264
|
-
assert_match(/QFG_DEFINITELY_UNSET/, err.message)
|
|
265
|
-
assert_match(/a\.missing/, err.message)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def test_resolver_get_raises_env_var_parse_error_on_bad_int
|
|
269
|
-
cfg = make_provided_config(key: 'a.number', value_type: 'int', lookup: 'QFG_TEST_BAD_INT')
|
|
270
|
-
resolver = build_resolver(cfg)
|
|
271
|
-
|
|
272
|
-
with_env('QFG_TEST_BAD_INT', 'not_a_number') do
|
|
273
|
-
err = assert_raises(Quonfig::Errors::EnvVarParseError) do
|
|
274
|
-
resolver.get('a.number', Quonfig::Context.new({}))
|
|
275
|
-
end
|
|
276
|
-
assert_match(/a\.number/, err.message)
|
|
277
|
-
assert_match(/not_a_number/, err.message)
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def test_resolver_get_raises_env_var_parse_error_on_bad_double
|
|
282
|
-
cfg = make_provided_config(key: 'a.double', value_type: 'double', lookup: 'QFG_TEST_BAD_DOUBLE')
|
|
283
|
-
resolver = build_resolver(cfg)
|
|
284
|
-
|
|
285
|
-
with_env('QFG_TEST_BAD_DOUBLE', 'not_a_number') do
|
|
286
|
-
assert_raises(Quonfig::Errors::EnvVarParseError) do
|
|
287
|
-
resolver.get('a.double', Quonfig::Context.new({}))
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
end
|