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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +94 -0
  4. data/lib/quonfig/caching_http_connection.rb +3 -3
  5. data/lib/quonfig/client.rb +22 -27
  6. data/lib/quonfig/config_loader.rb +5 -1
  7. data/lib/quonfig/config_store.rb +10 -6
  8. data/lib/quonfig/context.rb +5 -4
  9. data/lib/quonfig/datadir.rb +4 -10
  10. data/lib/quonfig/dev_context.rb +4 -2
  11. data/lib/quonfig/duration.rb +2 -2
  12. data/lib/quonfig/encryption.rb +12 -16
  13. data/lib/quonfig/errors/invalid_environment_error.rb +1 -3
  14. data/lib/quonfig/errors/invalid_sdk_key_error.rb +6 -7
  15. data/lib/quonfig/errors/missing_env_var_error.rb +0 -3
  16. data/lib/quonfig/errors/missing_environment_error.rb +1 -1
  17. data/lib/quonfig/errors/uninitialized_error.rb +1 -1
  18. data/lib/quonfig/evaluation.rb +11 -8
  19. data/lib/quonfig/evaluator.rb +34 -37
  20. data/lib/quonfig/fixed_size_hash.rb +1 -0
  21. data/lib/quonfig/http_connection.rb +2 -4
  22. data/lib/quonfig/internal_logger.rb +63 -27
  23. data/lib/quonfig/murmer3.rb +2 -2
  24. data/lib/quonfig/options.rb +62 -75
  25. data/lib/quonfig/periodic_sync.rb +1 -1
  26. data/lib/quonfig/quonfig.rb +3 -3
  27. data/lib/quonfig/reason.rb +2 -1
  28. data/lib/quonfig/resolver.rb +8 -9
  29. data/lib/quonfig/semantic_logger_filter.rb +4 -3
  30. data/lib/quonfig/semver.rb +6 -8
  31. data/lib/quonfig/sse_config_client.rb +14 -15
  32. data/lib/quonfig/stdlib_formatter.rb +3 -3
  33. data/lib/quonfig/telemetry/context_shape_aggregator.rb +2 -3
  34. data/lib/quonfig/telemetry/example_contexts_aggregator.rb +1 -1
  35. data/lib/quonfig/telemetry/telemetry_reporter.rb +1 -0
  36. data/lib/quonfig/time_helpers.rb +2 -0
  37. data/lib/quonfig/version.rb +5 -0
  38. data/lib/quonfig.rb +2 -1
  39. data/quonfig.gemspec +29 -165
  40. metadata +24 -193
  41. data/.claude/rules/constitution.md +0 -81
  42. data/.claude/rules/git-safety.md +0 -11
  43. data/.claude/rules/issue-tracking.md +0 -13
  44. data/.claude/rules/testing-workflow.md +0 -28
  45. data/.envrc.sample +0 -3
  46. data/.github/CODEOWNERS +0 -2
  47. data/.github/pull_request_template.md +0 -8
  48. data/.github/workflows/release.yml +0 -49
  49. data/.github/workflows/ruby.yml +0 -60
  50. data/.github/workflows/test.yaml +0 -40
  51. data/.rubocop.yml +0 -13
  52. data/.tool-versions +0 -1
  53. data/CLAUDE.md +0 -29
  54. data/CODEOWNERS +0 -1
  55. data/Gemfile +0 -26
  56. data/Gemfile.lock +0 -177
  57. data/Rakefile +0 -64
  58. data/VERSION +0 -1
  59. data/dev/allocation_stats +0 -60
  60. data/dev/benchmark +0 -40
  61. data/dev/console +0 -12
  62. data/dev/script_setup.rb +0 -18
  63. data/test/fixtures/datafile.json +0 -87
  64. data/test/integration/test_context_precedence.rb +0 -112
  65. data/test/integration/test_datadir_environment.rb +0 -54
  66. data/test/integration/test_dev_overrides.rb +0 -40
  67. data/test/integration/test_enabled.rb +0 -478
  68. data/test/integration/test_enabled_with_contexts.rb +0 -64
  69. data/test/integration/test_get.rb +0 -136
  70. data/test/integration/test_get_feature_flag.rb +0 -28
  71. data/test/integration/test_get_or_raise.rb +0 -60
  72. data/test/integration/test_get_weighted_values.rb +0 -34
  73. data/test/integration/test_helpers.rb +0 -667
  74. data/test/integration/test_helpers_test.rb +0 -73
  75. data/test/integration/test_post.rb +0 -44
  76. data/test/integration/test_telemetry.rb +0 -170
  77. data/test/support/common_helpers.rb +0 -106
  78. data/test/support/mock_base_client.rb +0 -27
  79. data/test/support/mock_config_loader.rb +0 -1
  80. data/test/test_bound_client.rb +0 -109
  81. data/test/test_caching_http_connection.rb +0 -218
  82. data/test/test_client.rb +0 -255
  83. data/test/test_client_network_mode.rb +0 -136
  84. data/test/test_client_telemetry.rb +0 -175
  85. data/test/test_config_loader.rb +0 -70
  86. data/test/test_context.rb +0 -139
  87. data/test/test_context_shape.rb +0 -37
  88. data/test/test_context_shape_aggregator.rb +0 -126
  89. data/test/test_datadir.rb +0 -203
  90. data/test/test_details_getters.rb +0 -242
  91. data/test/test_dev_context.rb +0 -163
  92. data/test/test_duration.rb +0 -37
  93. data/test/test_encryption.rb +0 -16
  94. data/test/test_evaluation_summaries_aggregator.rb +0 -180
  95. data/test/test_evaluator.rb +0 -285
  96. data/test/test_example_contexts_aggregator.rb +0 -119
  97. data/test/test_exponential_backoff.rb +0 -44
  98. data/test/test_fixed_size_hash.rb +0 -119
  99. data/test/test_helper.rb +0 -17
  100. data/test/test_http_connection.rb +0 -81
  101. data/test/test_internal_logger.rb +0 -34
  102. data/test/test_options.rb +0 -198
  103. data/test/test_rate_limit_cache.rb +0 -44
  104. data/test/test_reason.rb +0 -79
  105. data/test/test_rename.rb +0 -65
  106. data/test/test_resolver.rb +0 -291
  107. data/test/test_semantic_logger_filter.rb +0 -144
  108. data/test/test_semver.rb +0 -108
  109. data/test/test_should_log.rb +0 -186
  110. data/test/test_sse_config_client.rb +0 -297
  111. data/test/test_stdlib_formatter.rb +0 -195
  112. data/test/test_telemetry_reporter.rb +0 -209
  113. data/test/test_typed_getters.rb +0 -131
  114. data/test/test_types.rb +0 -141
  115. 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
@@ -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
@@ -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
@@ -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