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
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
|
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
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestEvaluationSummariesAggregator < Minitest::Test
|
|
6
|
-
def make_aggregator(max_keys: 100)
|
|
7
|
-
Quonfig::Telemetry::EvaluationSummariesAggregator.new(max_keys: max_keys)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def record_eval(agg, overrides = {})
|
|
11
|
-
defaults = {
|
|
12
|
-
config_id: 'c1',
|
|
13
|
-
config_key: 'my-test-key',
|
|
14
|
-
config_type: 'config',
|
|
15
|
-
conditional_value_index: 0,
|
|
16
|
-
weighted_value_index: nil,
|
|
17
|
-
selected_value: 'hello',
|
|
18
|
-
reason: 1
|
|
19
|
-
}
|
|
20
|
-
agg.record(**defaults.merge(overrides))
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def test_record_dedupes_identical_evaluations_into_one_counter
|
|
24
|
-
agg = make_aggregator
|
|
25
|
-
|
|
26
|
-
record_eval(agg)
|
|
27
|
-
record_eval(agg)
|
|
28
|
-
record_eval(agg)
|
|
29
|
-
|
|
30
|
-
event = agg.drain_event
|
|
31
|
-
refute_nil event
|
|
32
|
-
summaries = event['summaries']['summaries']
|
|
33
|
-
assert_equal 1, summaries.size
|
|
34
|
-
|
|
35
|
-
counters = summaries[0]['counters']
|
|
36
|
-
assert_equal 1, counters.size
|
|
37
|
-
assert_equal 3, counters[0]['count']
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def test_record_creates_separate_counters_for_different_rule_indexes
|
|
41
|
-
agg = make_aggregator
|
|
42
|
-
|
|
43
|
-
record_eval(agg, conditional_value_index: 0)
|
|
44
|
-
record_eval(agg, conditional_value_index: 1)
|
|
45
|
-
record_eval(agg, conditional_value_index: 1)
|
|
46
|
-
|
|
47
|
-
summaries = agg.drain_event['summaries']['summaries']
|
|
48
|
-
assert_equal 1, summaries.size
|
|
49
|
-
|
|
50
|
-
counters = summaries[0]['counters']
|
|
51
|
-
assert_equal 2, counters.size
|
|
52
|
-
|
|
53
|
-
by_idx = counters.each_with_object({}) { |c, h| h[c['conditionalValueIndex']] = c['count'] }
|
|
54
|
-
assert_equal 1, by_idx[0]
|
|
55
|
-
assert_equal 2, by_idx[1]
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def test_record_groups_by_config_key_and_type
|
|
59
|
-
agg = make_aggregator
|
|
60
|
-
|
|
61
|
-
record_eval(agg, config_key: 'alpha', config_type: 'config')
|
|
62
|
-
record_eval(agg, config_key: 'alpha', config_type: 'config')
|
|
63
|
-
record_eval(agg, config_key: 'beta', config_type: 'feature_flag')
|
|
64
|
-
|
|
65
|
-
summaries = agg.drain_event['summaries']['summaries']
|
|
66
|
-
assert_equal 2, summaries.size
|
|
67
|
-
|
|
68
|
-
by_key = summaries.each_with_object({}) { |s, h| h[s['key']] = s }
|
|
69
|
-
assert_equal 'config', by_key['alpha']['type']
|
|
70
|
-
assert_equal 'feature_flag', by_key['beta']['type']
|
|
71
|
-
assert_equal 2, by_key['alpha']['counters'][0]['count']
|
|
72
|
-
assert_equal 1, by_key['beta']['counters'][0]['count']
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def test_drain_event_nil_when_empty
|
|
76
|
-
agg = make_aggregator
|
|
77
|
-
assert_nil agg.drain_event
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def test_drain_clears_state
|
|
81
|
-
agg = make_aggregator
|
|
82
|
-
record_eval(agg)
|
|
83
|
-
|
|
84
|
-
refute_nil agg.drain_event
|
|
85
|
-
assert_nil agg.drain_event, 'second drain with no new records should be nil'
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def test_drain_event_wire_shape
|
|
89
|
-
agg = make_aggregator
|
|
90
|
-
|
|
91
|
-
record_eval(agg,
|
|
92
|
-
config_id: 'cid-42',
|
|
93
|
-
config_key: 'my-test-key',
|
|
94
|
-
config_type: 'config',
|
|
95
|
-
conditional_value_index: 1,
|
|
96
|
-
weighted_value_index: nil,
|
|
97
|
-
selected_value: 'my-test-value',
|
|
98
|
-
reason: 2)
|
|
99
|
-
|
|
100
|
-
event = agg.drain_event
|
|
101
|
-
refute_nil event
|
|
102
|
-
assert event.key?('summaries'), 'top-level event key is summaries'
|
|
103
|
-
|
|
104
|
-
inner = event['summaries']
|
|
105
|
-
assert_kind_of Integer, inner['start']
|
|
106
|
-
assert_kind_of Integer, inner['end']
|
|
107
|
-
assert inner['end'] >= inner['start']
|
|
108
|
-
|
|
109
|
-
summary = inner['summaries'][0]
|
|
110
|
-
assert_equal 'my-test-key', summary['key']
|
|
111
|
-
assert_equal 'config', summary['type']
|
|
112
|
-
|
|
113
|
-
counter = summary['counters'][0]
|
|
114
|
-
assert_equal 'cid-42', counter['configId']
|
|
115
|
-
assert_equal 1, counter['conditionalValueIndex']
|
|
116
|
-
assert_equal 0, counter['configRowIndex']
|
|
117
|
-
assert_equal 1, counter['count']
|
|
118
|
-
assert_equal 2, counter['reason']
|
|
119
|
-
assert_equal({ 'string' => 'my-test-value' }, counter['selectedValue'])
|
|
120
|
-
refute counter.key?('weightedValueIndex'),
|
|
121
|
-
'weightedValueIndex omitted when nil'
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def test_selected_value_wrapper_keys_match_prefab_shape
|
|
125
|
-
agg = make_aggregator
|
|
126
|
-
|
|
127
|
-
record_eval(agg, selected_value: true, conditional_value_index: 0)
|
|
128
|
-
record_eval(agg, selected_value: 3, conditional_value_index: 1)
|
|
129
|
-
record_eval(agg, selected_value: 1.5, conditional_value_index: 2)
|
|
130
|
-
record_eval(agg, selected_value: 'hi', conditional_value_index: 3)
|
|
131
|
-
record_eval(agg, selected_value: %w[a b], conditional_value_index: 4)
|
|
132
|
-
|
|
133
|
-
counters = agg.drain_event['summaries']['summaries'][0]['counters']
|
|
134
|
-
by_idx = counters.each_with_object({}) { |c, h| h[c['conditionalValueIndex']] = c['selectedValue'] }
|
|
135
|
-
|
|
136
|
-
assert_equal({ 'bool' => true }, by_idx[0])
|
|
137
|
-
assert_equal({ 'int' => 3 }, by_idx[1])
|
|
138
|
-
assert_equal({ 'double' => 1.5 }, by_idx[2])
|
|
139
|
-
assert_equal({ 'string' => 'hi' }, by_idx[3])
|
|
140
|
-
assert_equal({ 'stringList' => %w[a b] }, by_idx[4])
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def test_weighted_value_index_included_when_present
|
|
144
|
-
agg = make_aggregator
|
|
145
|
-
|
|
146
|
-
record_eval(agg, weighted_value_index: 2, reason: 3)
|
|
147
|
-
|
|
148
|
-
counter = agg.drain_event['summaries']['summaries'][0]['counters'][0]
|
|
149
|
-
assert_equal 2, counter['weightedValueIndex']
|
|
150
|
-
assert_equal 3, counter['reason']
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def test_log_level_evaluations_are_excluded
|
|
154
|
-
agg = make_aggregator
|
|
155
|
-
|
|
156
|
-
record_eval(agg, config_type: 'log_level')
|
|
157
|
-
|
|
158
|
-
assert_nil agg.drain_event
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def test_record_caps_at_max_keys
|
|
162
|
-
agg = make_aggregator(max_keys: 2)
|
|
163
|
-
|
|
164
|
-
record_eval(agg, config_key: 'a')
|
|
165
|
-
record_eval(agg, config_key: 'b')
|
|
166
|
-
record_eval(agg, config_key: 'c') # dropped — at cap
|
|
167
|
-
|
|
168
|
-
summaries = agg.drain_event['summaries']['summaries']
|
|
169
|
-
keys = summaries.map { |s| s['key'] }.sort
|
|
170
|
-
assert_equal %w[a b], keys
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def test_noop_when_max_keys_zero
|
|
174
|
-
agg = make_aggregator(max_keys: 0)
|
|
175
|
-
|
|
176
|
-
record_eval(agg)
|
|
177
|
-
|
|
178
|
-
assert_nil agg.drain_event
|
|
179
|
-
end
|
|
180
|
-
end
|