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
data/test/test_datadir.rb
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'tmpdir'
|
|
6
|
-
require 'fileutils'
|
|
7
|
-
|
|
8
|
-
# qfg-dk6.18 — Quonfig::Datadir loads JSON config files from a workspace
|
|
9
|
-
# directory and produces a ConfigEnvelope (and a populated ConfigStore) for
|
|
10
|
-
# offline / datadir mode. Mirrors sdk-node/src/datadir.ts.
|
|
11
|
-
class TestDatadir < Minitest::Test
|
|
12
|
-
CONFIG_SUBDIRS = %w[configs feature-flags segments schemas log-levels].freeze
|
|
13
|
-
|
|
14
|
-
def setup
|
|
15
|
-
@tmpdir = Dir.mktmpdir('quonfig-datadir-test')
|
|
16
|
-
CONFIG_SUBDIRS.each { |sub| FileUtils.mkdir_p(File.join(@tmpdir, sub)) }
|
|
17
|
-
File.write(File.join(@tmpdir, 'quonfig.json'), JSON.generate({ environments: %w[Production Staging] }))
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def teardown
|
|
21
|
-
FileUtils.remove_entry(@tmpdir) if @tmpdir && Dir.exist?(@tmpdir)
|
|
22
|
-
ENV.delete('QUONFIG_ENVIRONMENT')
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def write_config(subdir, filename, body)
|
|
26
|
-
File.write(File.join(@tmpdir, subdir, filename), JSON.generate(body))
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def sample_config(key, environment_id: 'Production')
|
|
30
|
-
{
|
|
31
|
-
'id' => '111',
|
|
32
|
-
'key' => key,
|
|
33
|
-
'type' => 'config',
|
|
34
|
-
'valueType' => 'string',
|
|
35
|
-
'sendToClientSdk' => false,
|
|
36
|
-
'default' => { 'rules' => [{ 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }], 'value' => { 'type' => 'string', 'value' => 'hello' } }] },
|
|
37
|
-
'environments' => [{ 'id' => environment_id, 'rules' => [{ 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }], 'value' => { 'type' => 'string', 'value' => 'env-hello' } }] }]
|
|
38
|
-
}
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def sample_flag(key)
|
|
42
|
-
{
|
|
43
|
-
'id' => '222',
|
|
44
|
-
'key' => key,
|
|
45
|
-
'type' => 'feature_flag',
|
|
46
|
-
'valueType' => 'bool',
|
|
47
|
-
'default' => { 'rules' => [{ 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }], 'value' => { 'type' => 'bool', 'value' => false } }] },
|
|
48
|
-
'environments' => []
|
|
49
|
-
}
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def test_load_envelope_returns_config_envelope
|
|
53
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
54
|
-
|
|
55
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
56
|
-
|
|
57
|
-
assert_instance_of Quonfig::ConfigEnvelope, envelope
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def test_load_envelope_reads_configs_from_each_subdir
|
|
61
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
62
|
-
write_config('feature-flags', 'b.flag.json', sample_flag('b.flag'))
|
|
63
|
-
write_config('log-levels', 'c.log.json', sample_config('c.log'))
|
|
64
|
-
write_config('segments', 'd.segment.json', sample_config('d.segment'))
|
|
65
|
-
|
|
66
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
67
|
-
|
|
68
|
-
keys = envelope.configs.map { |c| c['key'] }
|
|
69
|
-
assert_equal %w[a.config c.log d.segment b.flag].sort, keys.sort
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def test_load_envelope_skips_non_json_and_missing_subdirs
|
|
73
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
74
|
-
File.write(File.join(@tmpdir, 'configs', 'README.md'), '# ignore me')
|
|
75
|
-
FileUtils.rm_rf(File.join(@tmpdir, 'segments'))
|
|
76
|
-
|
|
77
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
78
|
-
|
|
79
|
-
keys = envelope.configs.map { |c| c['key'] }
|
|
80
|
-
assert_equal ['a.config'], keys
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def test_load_envelope_picks_environment_block_by_id
|
|
84
|
-
write_config('configs', 'a.config.json', sample_config('a.config', environment_id: 'Production'))
|
|
85
|
-
|
|
86
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
87
|
-
|
|
88
|
-
cfg = envelope.configs.first
|
|
89
|
-
refute_nil cfg['environment'], 'expected environment field to be populated'
|
|
90
|
-
assert_equal 'Production', cfg['environment']['id']
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def test_load_envelope_environment_field_nil_when_no_match
|
|
94
|
-
write_config('configs', 'a.config.json', sample_config('a.config', environment_id: 'Other'))
|
|
95
|
-
|
|
96
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
97
|
-
|
|
98
|
-
cfg = envelope.configs.first
|
|
99
|
-
assert_nil cfg['environment']
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def test_load_envelope_send_to_client_sdk_true_for_feature_flag
|
|
103
|
-
write_config('feature-flags', 'b.flag.json', sample_flag('b.flag'))
|
|
104
|
-
|
|
105
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
106
|
-
|
|
107
|
-
cfg = envelope.configs.first
|
|
108
|
-
assert_equal true, cfg['sendToClientSdk'], 'feature_flag must always sendToClientSdk=true'
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def test_load_envelope_default_rules_when_missing
|
|
112
|
-
raw = sample_config('a.config')
|
|
113
|
-
raw.delete('default')
|
|
114
|
-
write_config('configs', 'a.config.json', raw)
|
|
115
|
-
|
|
116
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
117
|
-
|
|
118
|
-
cfg = envelope.configs.first
|
|
119
|
-
assert_equal({ 'rules' => [] }, cfg['default'])
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def test_load_envelope_meta_includes_version_and_environment
|
|
123
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
124
|
-
|
|
125
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
126
|
-
|
|
127
|
-
assert_equal "datadir:#{@tmpdir}", envelope.meta['version']
|
|
128
|
-
assert_equal 'Production', envelope.meta['environment']
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def test_resolve_environment_from_env_var
|
|
132
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
133
|
-
ENV['QUONFIG_ENVIRONMENT'] = 'Production'
|
|
134
|
-
|
|
135
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, nil)
|
|
136
|
-
|
|
137
|
-
assert_equal 'Production', envelope.meta['environment']
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def test_constructor_environment_supersedes_env_var
|
|
141
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
142
|
-
ENV['QUONFIG_ENVIRONMENT'] = 'Staging'
|
|
143
|
-
|
|
144
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'Production')
|
|
145
|
-
|
|
146
|
-
assert_equal 'Production', envelope.meta['environment']
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def test_raises_when_no_environment
|
|
150
|
-
err = assert_raises(Quonfig::Errors::MissingEnvironmentError) do
|
|
151
|
-
Quonfig::Datadir.load_envelope(@tmpdir, nil)
|
|
152
|
-
end
|
|
153
|
-
assert_match(/Environment required for datadir mode/, err.message)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def test_raises_when_quonfig_json_missing
|
|
157
|
-
File.delete(File.join(@tmpdir, 'quonfig.json'))
|
|
158
|
-
|
|
159
|
-
err = assert_raises(ArgumentError) { Quonfig::Datadir.load_envelope(@tmpdir, 'Production') }
|
|
160
|
-
assert_match(/missing quonfig\.json/, err.message)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def test_raises_when_environment_not_in_workspace
|
|
164
|
-
err = assert_raises(Quonfig::Errors::InvalidEnvironmentError) do
|
|
165
|
-
Quonfig::Datadir.load_envelope(@tmpdir, 'NotAnEnv')
|
|
166
|
-
end
|
|
167
|
-
assert_match(/Environment "NotAnEnv" not found/, err.message)
|
|
168
|
-
assert_match(/Production/, err.message)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def test_allows_any_environment_when_quonfig_json_environments_empty
|
|
172
|
-
File.write(File.join(@tmpdir, 'quonfig.json'), JSON.generate({ environments: [] }))
|
|
173
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
174
|
-
|
|
175
|
-
envelope = Quonfig::Datadir.load_envelope(@tmpdir, 'AnythingGoes')
|
|
176
|
-
|
|
177
|
-
assert_equal 'AnythingGoes', envelope.meta['environment']
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def test_load_store_returns_populated_config_store
|
|
181
|
-
write_config('configs', 'a.config.json', sample_config('a.config'))
|
|
182
|
-
write_config('feature-flags', 'b.flag.json', sample_flag('b.flag'))
|
|
183
|
-
|
|
184
|
-
store = Quonfig::Datadir.load_store(@tmpdir, 'Production')
|
|
185
|
-
|
|
186
|
-
assert_instance_of Quonfig::ConfigStore, store
|
|
187
|
-
assert_equal %w[a.config b.flag].sort, store.keys.sort
|
|
188
|
-
assert_equal 'a.config', store.get('a.config')['key']
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Verification against the real integration-test-data fixture (path-based).
|
|
192
|
-
# Skips when the sibling repo is not present (e.g. limited CI checkouts).
|
|
193
|
-
def test_load_store_with_integration_test_data_fixture
|
|
194
|
-
fixture = File.expand_path('../../integration-test-data/data/integration-tests', __dir__)
|
|
195
|
-
skip "integration-test-data not present at #{fixture}" unless Dir.exist?(fixture)
|
|
196
|
-
|
|
197
|
-
store = Quonfig::Datadir.load_store(fixture, 'Production')
|
|
198
|
-
|
|
199
|
-
refute_empty store.keys, 'expected at least one config key from integration-test-data'
|
|
200
|
-
assert(store.keys.any? { |k| k.start_with?('a.') } || store.keys.include?('already.in.use'),
|
|
201
|
-
"expected to find familiar fixture keys; got: #{store.keys.first(5).inspect}")
|
|
202
|
-
end
|
|
203
|
-
end
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
# Coverage for the *_details getters on Quonfig::Client and Quonfig::BoundClient.
|
|
6
|
-
# These return Quonfig::EvaluationDetails carrying an OpenFeature-aligned
|
|
7
|
-
# +reason+ and (on the error path) error_code / error_message. They never raise.
|
|
8
|
-
class TestDetailsGetters < Minitest::Test
|
|
9
|
-
Details = Quonfig::EvaluationDetails
|
|
10
|
-
|
|
11
|
-
INTEGRATION_FIXTURE_DIR = File.expand_path(
|
|
12
|
-
'../../integration-test-data/data/integration-tests', __dir__
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
# ---- Helpers --------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
def fixture_client
|
|
18
|
-
skip "integration-test-data sibling missing at #{INTEGRATION_FIXTURE_DIR}" unless Dir.exist?(INTEGRATION_FIXTURE_DIR)
|
|
19
|
-
|
|
20
|
-
Quonfig::Client.new(
|
|
21
|
-
Quonfig::Options.new(
|
|
22
|
-
datadir: INTEGRATION_FIXTURE_DIR,
|
|
23
|
-
environment: 'Production',
|
|
24
|
-
enable_sse: false,
|
|
25
|
-
enable_polling: false
|
|
26
|
-
)
|
|
27
|
-
)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# An ALWAYS_TRUE-only config: no targeting rules anywhere -> STATIC.
|
|
31
|
-
def make_static_config(key:, value:, type:)
|
|
32
|
-
{
|
|
33
|
-
'id' => '1',
|
|
34
|
-
'key' => key,
|
|
35
|
-
'type' => 'config',
|
|
36
|
-
'valueType' => type,
|
|
37
|
-
'sendToClientSdk' => false,
|
|
38
|
-
'default' => {
|
|
39
|
-
'rules' => [
|
|
40
|
-
{
|
|
41
|
-
'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
42
|
-
'value' => { 'type' => type, 'value' => value }
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
},
|
|
46
|
-
'environment' => nil
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def static_client(key:, value:, type:)
|
|
51
|
-
store = Quonfig::ConfigStore.new
|
|
52
|
-
store.set(key, make_static_config(key: key, value: value, type: type))
|
|
53
|
-
Quonfig::Client.new(Quonfig::Options.new, store: store)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# ---- STATIC reason --------------------------------------------------
|
|
57
|
-
|
|
58
|
-
def test_get_bool_details_static_reason_for_always_true_only_config
|
|
59
|
-
client = static_client(key: 'plain', value: true, type: 'bool')
|
|
60
|
-
details = client.get_bool_details('plain')
|
|
61
|
-
assert_kind_of Details, details
|
|
62
|
-
assert_equal true, details.value
|
|
63
|
-
assert_equal Details::REASON_STATIC, details.reason
|
|
64
|
-
assert_nil details.error_code
|
|
65
|
-
assert_nil details.error_message
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def test_get_string_details_static_reason
|
|
69
|
-
client = static_client(key: 'plain.s', value: 'hi', type: 'string')
|
|
70
|
-
details = client.get_string_details('plain.s')
|
|
71
|
-
assert_equal 'hi', details.value
|
|
72
|
-
assert_equal Details::REASON_STATIC, details.reason
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def test_static_reason_via_integration_fixture_always_true
|
|
76
|
-
client = fixture_client
|
|
77
|
-
details = client.get_bool_details('always.true')
|
|
78
|
-
assert_equal true, details.value
|
|
79
|
-
assert_equal Details::REASON_STATIC, details.reason
|
|
80
|
-
ensure
|
|
81
|
-
client&.stop
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# ---- TARGETING_MATCH reason ----------------------------------------
|
|
85
|
-
|
|
86
|
-
def test_targeting_match_reason_via_integration_fixture
|
|
87
|
-
client = fixture_client
|
|
88
|
-
details = client.get_bool_details('of.targeting', context: { 'user' => { 'plan' => 'pro' } })
|
|
89
|
-
assert_equal true, details.value
|
|
90
|
-
assert_equal Details::REASON_TARGETING_MATCH, details.reason
|
|
91
|
-
ensure
|
|
92
|
-
client&.stop
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def test_targeting_match_falls_through_to_default_branch_still_targeting
|
|
96
|
-
# The of.targeting fixture has a property-match rule + an ALWAYS_TRUE
|
|
97
|
-
# fallback. Even when the fallback wins, the *config* has targeting rules,
|
|
98
|
-
# so wire_reason / of_reason returns TARGETING_MATCH.
|
|
99
|
-
client = fixture_client
|
|
100
|
-
details = client.get_bool_details('of.targeting', context: { 'user' => { 'plan' => 'free' } })
|
|
101
|
-
assert_equal false, details.value
|
|
102
|
-
assert_equal Details::REASON_TARGETING_MATCH, details.reason
|
|
103
|
-
ensure
|
|
104
|
-
client&.stop
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# ---- SPLIT reason ---------------------------------------------------
|
|
108
|
-
|
|
109
|
-
def test_split_reason_via_integration_fixture
|
|
110
|
-
client = fixture_client
|
|
111
|
-
# Pick a deterministic targetingKey so the outcome is reproducible.
|
|
112
|
-
details = client.get_string_details('of.weighted', context: { 'user' => { 'id' => 'user-123' } })
|
|
113
|
-
assert_equal Details::REASON_SPLIT, details.reason
|
|
114
|
-
assert_includes %w[variant-a variant-b], details.value
|
|
115
|
-
ensure
|
|
116
|
-
client&.stop
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# ---- DEFAULT reason -------------------------------------------------
|
|
120
|
-
|
|
121
|
-
def test_default_reason_when_no_rule_matches
|
|
122
|
-
# Config exists but no rule matches against the empty context.
|
|
123
|
-
config = {
|
|
124
|
-
'id' => '9',
|
|
125
|
-
'key' => 'no.match.here',
|
|
126
|
-
'type' => 'config',
|
|
127
|
-
'valueType' => 'string',
|
|
128
|
-
'sendToClientSdk' => false,
|
|
129
|
-
'default' => {
|
|
130
|
-
'rules' => [
|
|
131
|
-
{
|
|
132
|
-
'criteria' => [
|
|
133
|
-
{
|
|
134
|
-
'propertyName' => 'user.plan',
|
|
135
|
-
'operator' => 'PROP_IS_ONE_OF',
|
|
136
|
-
'valueToMatch' => { 'type' => 'string_list', 'value' => ['enterprise'] }
|
|
137
|
-
}
|
|
138
|
-
],
|
|
139
|
-
'value' => { 'type' => 'string', 'value' => 'gold' }
|
|
140
|
-
}
|
|
141
|
-
]
|
|
142
|
-
},
|
|
143
|
-
'environment' => nil
|
|
144
|
-
}
|
|
145
|
-
store = Quonfig::ConfigStore.new
|
|
146
|
-
store.set('no.match.here', config)
|
|
147
|
-
client = Quonfig::Client.new(Quonfig::Options.new, store: store)
|
|
148
|
-
|
|
149
|
-
details = client.get_string_details('no.match.here')
|
|
150
|
-
assert_nil details.value
|
|
151
|
-
assert_equal Details::REASON_DEFAULT, details.reason
|
|
152
|
-
assert_nil details.error_code
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# ---- ERROR / FLAG_NOT_FOUND ----------------------------------------
|
|
156
|
-
|
|
157
|
-
def test_flag_not_found_returns_error_details
|
|
158
|
-
client = Quonfig::Client.new(Quonfig::Options.new, store: Quonfig::ConfigStore.new)
|
|
159
|
-
details = client.get_bool_details('does.not.exist')
|
|
160
|
-
assert_nil details.value
|
|
161
|
-
assert_equal Details::REASON_ERROR, details.reason
|
|
162
|
-
assert_equal Details::ERROR_FLAG_NOT_FOUND, details.error_code
|
|
163
|
-
refute_nil details.error_message
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def test_flag_not_found_for_string_details
|
|
167
|
-
client = Quonfig::Client.new(Quonfig::Options.new, store: Quonfig::ConfigStore.new)
|
|
168
|
-
details = client.get_string_details('nope')
|
|
169
|
-
assert_equal Details::REASON_ERROR, details.reason
|
|
170
|
-
assert_equal Details::ERROR_FLAG_NOT_FOUND, details.error_code
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# ---- ERROR / TYPE_MISMATCH -----------------------------------------
|
|
174
|
-
|
|
175
|
-
def test_type_mismatch_for_int_when_value_is_string
|
|
176
|
-
client = static_client(key: 'wrong.type', value: 'oops', type: 'string')
|
|
177
|
-
details = client.get_int_details('wrong.type')
|
|
178
|
-
assert_nil details.value
|
|
179
|
-
assert_equal Details::REASON_ERROR, details.reason
|
|
180
|
-
assert_equal Details::ERROR_TYPE_MISMATCH, details.error_code
|
|
181
|
-
refute_nil details.error_message
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def test_type_mismatch_for_bool_when_value_is_string
|
|
185
|
-
client = static_client(key: 'bool.miss', value: 'true', type: 'string')
|
|
186
|
-
details = client.get_bool_details('bool.miss')
|
|
187
|
-
assert_equal Details::REASON_ERROR, details.reason
|
|
188
|
-
assert_equal Details::ERROR_TYPE_MISMATCH, details.error_code
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def test_type_mismatch_for_string_list_when_value_is_string
|
|
192
|
-
client = static_client(key: 'list.miss', value: 'a,b', type: 'string')
|
|
193
|
-
details = client.get_string_list_details('list.miss')
|
|
194
|
-
assert_equal Details::REASON_ERROR, details.reason
|
|
195
|
-
assert_equal Details::ERROR_TYPE_MISMATCH, details.error_code
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# ---- json passes through unchanged ---------------------------------
|
|
199
|
-
|
|
200
|
-
def test_get_json_details_returns_hash_with_static_reason
|
|
201
|
-
payload = { 'a' => 1, 'b' => [1, 2] }
|
|
202
|
-
client = static_client(key: 'shape', value: payload, type: 'json')
|
|
203
|
-
details = client.get_json_details('shape')
|
|
204
|
-
assert_equal payload, details.value
|
|
205
|
-
assert_equal Details::REASON_STATIC, details.reason
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# ---- Float / Int success path -------------------------------------
|
|
209
|
-
|
|
210
|
-
def test_get_int_details_static_reason
|
|
211
|
-
client = static_client(key: 'i', value: 42, type: 'int')
|
|
212
|
-
details = client.get_int_details('i')
|
|
213
|
-
assert_equal 42, details.value
|
|
214
|
-
assert_equal Details::REASON_STATIC, details.reason
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def test_get_float_details_static_reason
|
|
218
|
-
client = static_client(key: 'f', value: 3.14, type: 'double')
|
|
219
|
-
details = client.get_float_details('f')
|
|
220
|
-
assert_in_delta 3.14, details.value, 1e-9
|
|
221
|
-
assert_equal Details::REASON_STATIC, details.reason
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def test_get_string_list_details_static_reason
|
|
225
|
-
client = static_client(key: 'sl', value: %w[a b], type: 'string_list')
|
|
226
|
-
details = client.get_string_list_details('sl')
|
|
227
|
-
assert_equal %w[a b], details.value
|
|
228
|
-
assert_equal Details::REASON_STATIC, details.reason
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# ---- BoundClient mirror --------------------------------------------
|
|
232
|
-
|
|
233
|
-
def test_bound_client_get_bool_details_passes_context
|
|
234
|
-
client = fixture_client
|
|
235
|
-
bound = client.in_context('user' => { 'plan' => 'pro' })
|
|
236
|
-
details = bound.get_bool_details('of.targeting')
|
|
237
|
-
assert_equal true, details.value
|
|
238
|
-
assert_equal Details::REASON_TARGETING_MATCH, details.reason
|
|
239
|
-
ensure
|
|
240
|
-
client&.stop
|
|
241
|
-
end
|
|
242
|
-
end
|
data/test/test_dev_context.rb
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'tmpdir'
|
|
6
|
-
require 'fileutils'
|
|
7
|
-
|
|
8
|
-
# qfg-pj0.5 — Dev-context injection. When enable_quonfig_user_context: true
|
|
9
|
-
# (or env var QUONFIG_DEV_CONTEXT=true), the SDK reads ~/.quonfig/tokens.json
|
|
10
|
-
# (written by `qfg login`) and merges {'quonfig-user' => {'email' => ...}}
|
|
11
|
-
# into globalContext. Customer-supplied keys win on collision.
|
|
12
|
-
#
|
|
13
|
-
# Mirror of sdk-node qfg-pj0.3 / sdk-go qfg-pj0.4.
|
|
14
|
-
class TestDevContext < Minitest::Test
|
|
15
|
-
def setup
|
|
16
|
-
super
|
|
17
|
-
@tmphome = Dir.mktmpdir('quonfig-dev-ctx-')
|
|
18
|
-
FileUtils.mkdir_p(File.join(@tmphome, '.quonfig'))
|
|
19
|
-
@old_home = ENV.fetch('HOME', nil)
|
|
20
|
-
ENV['HOME'] = @tmphome
|
|
21
|
-
ENV.delete('QUONFIG_DEV_CONTEXT')
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def teardown
|
|
25
|
-
ENV['HOME'] = @old_home
|
|
26
|
-
ENV.delete('QUONFIG_DEV_CONTEXT')
|
|
27
|
-
FileUtils.remove_entry(@tmphome) if @tmphome && Dir.exist?(@tmphome)
|
|
28
|
-
super
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def write_tokens(payload)
|
|
32
|
-
File.write(File.join(@tmphome, '.quonfig', 'tokens.json'), JSON.generate(payload))
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def global_context_of(client)
|
|
36
|
-
client.instance_variable_get(:@global_context)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# 1. RED: injects quonfig-user.email when option enabled and file exists
|
|
40
|
-
def test_injects_quonfig_user_email_when_option_enabled
|
|
41
|
-
write_tokens(userEmail: 'bob@foo.com', accessToken: 'x', refreshToken: 'y', expiresAt: 0)
|
|
42
|
-
|
|
43
|
-
client = Quonfig::Client.new(
|
|
44
|
-
Quonfig::Options.new(enable_quonfig_user_context: true),
|
|
45
|
-
store: Quonfig::ConfigStore.new
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
assert_equal({ 'quonfig-user' => { 'email' => 'bob@foo.com' } }, global_context_of(client))
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# 2. RED: no-op when option disabled and no env var
|
|
52
|
-
def test_no_op_when_option_disabled
|
|
53
|
-
write_tokens(userEmail: 'bob@foo.com')
|
|
54
|
-
|
|
55
|
-
client = Quonfig::Client.new(
|
|
56
|
-
Quonfig::Options.new(global_context: { user: { 'plan' => 'pro' } }),
|
|
57
|
-
store: Quonfig::ConfigStore.new
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
assert_equal({ user: { 'plan' => 'pro' } }, global_context_of(client))
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# 3. RED: no-op when option enabled but file missing
|
|
64
|
-
def test_no_op_when_file_missing
|
|
65
|
-
# No tokens.json written.
|
|
66
|
-
client = Quonfig::Client.new(
|
|
67
|
-
Quonfig::Options.new(
|
|
68
|
-
enable_quonfig_user_context: true,
|
|
69
|
-
global_context: { user: { 'plan' => 'pro' } }
|
|
70
|
-
),
|
|
71
|
-
store: Quonfig::ConfigStore.new
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
assert_equal({ user: { 'plan' => 'pro' } }, global_context_of(client))
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# 4. RED: no-op when file unparseable; warning emitted; init succeeds
|
|
78
|
-
def test_no_op_when_file_unparseable
|
|
79
|
-
File.write(File.join(@tmphome, '.quonfig', 'tokens.json'), '{not valid json')
|
|
80
|
-
|
|
81
|
-
client = Quonfig::Client.new(
|
|
82
|
-
Quonfig::Options.new(enable_quonfig_user_context: true),
|
|
83
|
-
store: Quonfig::ConfigStore.new
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
assert_equal({}, global_context_of(client))
|
|
87
|
-
# The dev-context loader emits a warning to stderr that we want to verify.
|
|
88
|
-
assert_stderr(['quonfig'])
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# 5. RED: customer-supplied quonfig-user keys win on collision
|
|
92
|
-
def test_customer_global_context_wins
|
|
93
|
-
write_tokens(userEmail: 'bob@foo.com')
|
|
94
|
-
|
|
95
|
-
client = Quonfig::Client.new(
|
|
96
|
-
Quonfig::Options.new(
|
|
97
|
-
enable_quonfig_user_context: true,
|
|
98
|
-
global_context: { 'quonfig-user' => { 'email' => 'override@x.com' } }
|
|
99
|
-
),
|
|
100
|
-
store: Quonfig::ConfigStore.new
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
assert_equal({ 'quonfig-user' => { 'email' => 'override@x.com' } }, global_context_of(client))
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# 6. RED: env var QUONFIG_DEV_CONTEXT=true enables when option absent
|
|
107
|
-
def test_env_var_enables_when_option_absent
|
|
108
|
-
write_tokens(userEmail: 'bob@foo.com')
|
|
109
|
-
ENV['QUONFIG_DEV_CONTEXT'] = 'true'
|
|
110
|
-
|
|
111
|
-
client = Quonfig::Client.new(
|
|
112
|
-
Quonfig::Options.new,
|
|
113
|
-
store: Quonfig::ConfigStore.new
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
assert_equal({ 'quonfig-user' => { 'email' => 'bob@foo.com' } }, global_context_of(client))
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# 7. RED: integration — rule keyed on quonfig-user.email fires when injected
|
|
120
|
-
def test_attribute_reaches_eval_context
|
|
121
|
-
write_tokens(userEmail: 'bob@foo.com')
|
|
122
|
-
|
|
123
|
-
flag_config = {
|
|
124
|
-
'id' => 'cfg-flag',
|
|
125
|
-
'key' => 'my-flag',
|
|
126
|
-
'type' => 'feature_flag',
|
|
127
|
-
'valueType' => 'bool',
|
|
128
|
-
'sendToClientSdk' => false,
|
|
129
|
-
'default' => {
|
|
130
|
-
'rules' => [
|
|
131
|
-
{ 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }], 'value' => { 'type' => 'bool', 'value' => false } }
|
|
132
|
-
]
|
|
133
|
-
},
|
|
134
|
-
'environment' => {
|
|
135
|
-
'id' => 'Production',
|
|
136
|
-
'rules' => [
|
|
137
|
-
{
|
|
138
|
-
'criteria' => [{
|
|
139
|
-
'propertyName' => 'quonfig-user.email',
|
|
140
|
-
'operator' => 'PROP_IS_ONE_OF',
|
|
141
|
-
'valueToMatch' => { 'type' => 'string_list', 'value' => ['bob@foo.com'] }
|
|
142
|
-
}],
|
|
143
|
-
'value' => { 'type' => 'bool', 'value' => true }
|
|
144
|
-
},
|
|
145
|
-
{ 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }], 'value' => { 'type' => 'bool', 'value' => false } }
|
|
146
|
-
]
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
store = Quonfig::ConfigStore.new
|
|
151
|
-
store.set('my-flag', flag_config)
|
|
152
|
-
|
|
153
|
-
client = Quonfig::Client.new(
|
|
154
|
-
Quonfig::Options.new(
|
|
155
|
-
enable_quonfig_user_context: true,
|
|
156
|
-
environment: 'Production'
|
|
157
|
-
),
|
|
158
|
-
store: store
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
assert_equal true, client.get_bool('my-flag')
|
|
162
|
-
end
|
|
163
|
-
end
|
data/test/test_duration.rb
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class DurationTest < Minitest::Test
|
|
6
|
-
MINUTES_IN_SECONDS = 60
|
|
7
|
-
HOURS_IN_SECONDS = 60 * MINUTES_IN_SECONDS
|
|
8
|
-
DAYS_IN_SECONDS = 24 * HOURS_IN_SECONDS
|
|
9
|
-
|
|
10
|
-
TESTS = [
|
|
11
|
-
['PT0M0S', 0],
|
|
12
|
-
['PT6M', 6 * MINUTES_IN_SECONDS],
|
|
13
|
-
['PT90S', 90],
|
|
14
|
-
['P1D', DAYS_IN_SECONDS],
|
|
15
|
-
['PT1.5M', 1.5 * MINUTES_IN_SECONDS],
|
|
16
|
-
['P0.75D', 0.75 * DAYS_IN_SECONDS],
|
|
17
|
-
['PT1M90.3S', MINUTES_IN_SECONDS + 90.3],
|
|
18
|
-
['PT1H', HOURS_IN_SECONDS],
|
|
19
|
-
['PT1.3H', 1.3 * HOURS_IN_SECONDS],
|
|
20
|
-
['P1.5DT1.5M', 1.5 * DAYS_IN_SECONDS + MINUTES_IN_SECONDS * 1.5],
|
|
21
|
-
['P1.5DT1.5H1.5M', 1.5 * DAYS_IN_SECONDS + 1.5 * HOURS_IN_SECONDS + MINUTES_IN_SECONDS * 1.5],
|
|
22
|
-
['P1.5DT1.5H1.5M3.5S', 1.5 * DAYS_IN_SECONDS + 1.5 * HOURS_IN_SECONDS + MINUTES_IN_SECONDS * 1.5 + 3.5],
|
|
23
|
-
['P1DT2H3M4S', DAYS_IN_SECONDS + 2 * HOURS_IN_SECONDS + 3 * MINUTES_IN_SECONDS + 4],
|
|
24
|
-
['PT1H30M', HOURS_IN_SECONDS + 30 * MINUTES_IN_SECONDS],
|
|
25
|
-
['P0.5DT0.25H', 0.5 * DAYS_IN_SECONDS + 0.25 * HOURS_IN_SECONDS],
|
|
26
|
-
['PT15M30S', 15 * MINUTES_IN_SECONDS + 30],
|
|
27
|
-
['PT0.000347222H', 0.000347222 * HOURS_IN_SECONDS],
|
|
28
|
-
['PT23H59M59S', 23 * HOURS_IN_SECONDS + 59 * MINUTES_IN_SECONDS + 59],
|
|
29
|
-
['P0.25DT3.75H', 0.25 * DAYS_IN_SECONDS + 3.75 * HOURS_IN_SECONDS],
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
def test_parsing
|
|
33
|
-
TESTS.each do |test|
|
|
34
|
-
assert_equal test[1], Quonfig::Duration.parse(test[0]), "Failed parsing #{test[0]}"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
data/test/test_encryption.rb
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestEncryption < Minitest::Test
|
|
6
|
-
def test_encryption
|
|
7
|
-
secret = Quonfig::Encryption.generate_new_hex_key
|
|
8
|
-
|
|
9
|
-
enc = Quonfig::Encryption.new(secret)
|
|
10
|
-
|
|
11
|
-
clear_text = "hello world"
|
|
12
|
-
encrypted = enc.encrypt(clear_text)
|
|
13
|
-
decrypted = enc.decrypt(encrypted)
|
|
14
|
-
assert_equal clear_text, decrypted
|
|
15
|
-
end
|
|
16
|
-
end
|