quonfig 0.0.2
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 +7 -0
- data/.claude/rules/constitution.md +81 -0
- data/.claude/rules/git-safety.md +11 -0
- data/.claude/rules/issue-tracking.md +13 -0
- data/.claude/rules/testing-workflow.md +28 -0
- data/.envrc.sample +3 -0
- data/.github/CODEOWNERS +2 -0
- data/.github/pull_request_template.md +8 -0
- data/.github/workflows/push_gem.yml +49 -0
- data/.github/workflows/ruby.yml +60 -0
- data/.github/workflows/test.yaml +40 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +301 -0
- data/CLAUDE.md +29 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +177 -0
- data/LICENSE.txt +20 -0
- data/README.md +213 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/dev/allocation_stats +60 -0
- data/dev/benchmark +40 -0
- data/dev/console +12 -0
- data/dev/script_setup.rb +18 -0
- data/lib/quonfig/bound_client.rb +71 -0
- data/lib/quonfig/caching_http_connection.rb +95 -0
- data/lib/quonfig/client.rb +221 -0
- data/lib/quonfig/config_envelope.rb +5 -0
- data/lib/quonfig/config_loader.rb +103 -0
- data/lib/quonfig/config_store.rb +42 -0
- data/lib/quonfig/context.rb +101 -0
- data/lib/quonfig/datadir.rb +101 -0
- data/lib/quonfig/duration.rb +58 -0
- data/lib/quonfig/encryption.rb +74 -0
- data/lib/quonfig/error.rb +6 -0
- data/lib/quonfig/errors/env_var_parse_error.rb +11 -0
- data/lib/quonfig/errors/initialization_timeout_error.rb +12 -0
- data/lib/quonfig/errors/invalid_sdk_key_error.rb +19 -0
- data/lib/quonfig/errors/missing_default_error.rb +13 -0
- data/lib/quonfig/errors/missing_env_var_error.rb +11 -0
- data/lib/quonfig/errors/type_mismatch_error.rb +11 -0
- data/lib/quonfig/errors/uninitialized_error.rb +13 -0
- data/lib/quonfig/evaluation.rb +64 -0
- data/lib/quonfig/evaluator.rb +464 -0
- data/lib/quonfig/exponential_backoff.rb +21 -0
- data/lib/quonfig/fixed_size_hash.rb +14 -0
- data/lib/quonfig/http_connection.rb +46 -0
- data/lib/quonfig/internal_logger.rb +173 -0
- data/lib/quonfig/murmer3.rb +50 -0
- data/lib/quonfig/options.rb +194 -0
- data/lib/quonfig/periodic_sync.rb +74 -0
- data/lib/quonfig/quonfig.rb +58 -0
- data/lib/quonfig/rate_limit_cache.rb +41 -0
- data/lib/quonfig/reason.rb +39 -0
- data/lib/quonfig/resolver.rb +42 -0
- data/lib/quonfig/semantic_logger_filter.rb +90 -0
- data/lib/quonfig/semver.rb +132 -0
- data/lib/quonfig/sse_config_client.rb +135 -0
- data/lib/quonfig/time_helpers.rb +7 -0
- data/lib/quonfig/types.rb +56 -0
- data/lib/quonfig/weighted_value_resolver.rb +49 -0
- data/lib/quonfig.rb +57 -0
- data/quonfig.gemspec +149 -0
- data/scripts/generate_integration_tests.rb +362 -0
- data/test/fixtures/datafile.json +87 -0
- data/test/integration/test_context_precedence.rb +194 -0
- data/test/integration/test_datadir_environment.rb +76 -0
- data/test/integration/test_enabled.rb +784 -0
- data/test/integration/test_enabled_with_contexts.rb +94 -0
- data/test/integration/test_get.rb +224 -0
- data/test/integration/test_get_feature_flag.rb +34 -0
- data/test/integration/test_get_or_raise.rb +86 -0
- data/test/integration/test_get_weighted_values.rb +29 -0
- data/test/integration/test_helpers.rb +139 -0
- data/test/integration/test_helpers_test.rb +73 -0
- data/test/integration/test_post.rb +34 -0
- data/test/integration/test_telemetry.rb +114 -0
- data/test/support/common_helpers.rb +106 -0
- data/test/support/mock_base_client.rb +27 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_bound_client.rb +109 -0
- data/test/test_caching_http_connection.rb +218 -0
- data/test/test_client.rb +255 -0
- data/test/test_config_loader.rb +70 -0
- data/test/test_context.rb +136 -0
- data/test/test_datadir.rb +199 -0
- data/test/test_duration.rb +37 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluator.rb +285 -0
- data/test/test_exponential_backoff.rb +44 -0
- data/test/test_fixed_size_hash.rb +119 -0
- data/test/test_helper.rb +17 -0
- data/test/test_http_connection.rb +79 -0
- data/test/test_internal_logger.rb +34 -0
- data/test/test_options.rb +167 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_reason.rb +79 -0
- data/test/test_rename.rb +65 -0
- data/test/test_resolver.rb +144 -0
- data/test/test_semantic_logger_filter.rb +123 -0
- data/test/test_semver.rb +108 -0
- data/test/test_sse_config_client.rb +297 -0
- data/test/test_typed_getters.rb +131 -0
- data/test/test_types.rb +141 -0
- data/test/test_weighted_value_resolver.rb +84 -0
- metadata +311 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# AUTO-GENERATED from integration-test-data/tests/eval/enabled_with_contexts.yaml.
|
|
4
|
+
# Regenerate with `bundle exec ruby scripts/generate_integration_tests.rb`.
|
|
5
|
+
# Do NOT edit by hand — changes will be overwritten.
|
|
6
|
+
|
|
7
|
+
require 'test_helper'
|
|
8
|
+
require 'integration/test_helpers'
|
|
9
|
+
|
|
10
|
+
class TestEnabledWithContexts < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
@store = IntegrationTestHelpers.build_store("enabled_with_contexts")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# returns true from global context
|
|
16
|
+
def test_returns_true_from_global_context
|
|
17
|
+
begin
|
|
18
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
19
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.in-seg.segment-and", {"" => {"domain" => "prefab.cloud"}, "user" => {"key" => "michael"}}, true)
|
|
20
|
+
rescue Exception => e
|
|
21
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# returns false due to local context override
|
|
26
|
+
def test_returns_false_due_to_local_context_override
|
|
27
|
+
begin
|
|
28
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
29
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.in-seg.segment-and", {"" => {"domain" => "prefab.cloud"}, "user" => {"key" => "james"}}, false)
|
|
30
|
+
rescue Exception => e
|
|
31
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# returns false for untouched scope context
|
|
36
|
+
def test_returns_false_for_untouched_scope_context
|
|
37
|
+
begin
|
|
38
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
39
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.in-seg.segment-and", {"" => {"domain" => "example.com"}, "user" => {"key" => "nobody"}}, false)
|
|
40
|
+
rescue Exception => e
|
|
41
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# returns false due to partial scope context override of user.key
|
|
46
|
+
def test_returns_false_due_to_partial_scope_context_override_of_user_key
|
|
47
|
+
begin
|
|
48
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
49
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.in-seg.segment-and", {"" => {"domain" => "example.com"}, "user" => {"key" => "michael"}}, false)
|
|
50
|
+
rescue Exception => e
|
|
51
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# returns false due to partial scope context override of domain
|
|
56
|
+
def test_returns_false_due_to_partial_scope_context_override_of_domain
|
|
57
|
+
begin
|
|
58
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
59
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.in-seg.segment-and", {"" => {"domain" => "example.com", "key" => "prefab.cloud"}, "user" => {"key" => "nobody"}}, false)
|
|
60
|
+
rescue Exception => e
|
|
61
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# returns true due to full scope context override of user.key and domain
|
|
66
|
+
def test_returns_true_due_to_full_scope_context_override_of_user_key_and_domain
|
|
67
|
+
begin
|
|
68
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
69
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.in-seg.segment-and", {"" => {"domain" => "prefab.cloud"}, "user" => {"key" => "michael"}}, true)
|
|
70
|
+
rescue Exception => e
|
|
71
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# returns false for rule with different case on context property name
|
|
76
|
+
def test_returns_false_for_rule_with_different_case_on_context_property_name
|
|
77
|
+
begin
|
|
78
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
79
|
+
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"IsHuman" => "verified"}}, false)
|
|
80
|
+
rescue Exception => e
|
|
81
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# returns true for matching case on context property name
|
|
86
|
+
def test_returns_true_for_matching_case_on_context_property_name
|
|
87
|
+
begin
|
|
88
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
89
|
+
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
90
|
+
rescue Exception => e
|
|
91
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# AUTO-GENERATED from integration-test-data/tests/eval/get.yaml.
|
|
4
|
+
# Regenerate with `bundle exec ruby scripts/generate_integration_tests.rb`.
|
|
5
|
+
# Do NOT edit by hand — changes will be overwritten.
|
|
6
|
+
|
|
7
|
+
require 'test_helper'
|
|
8
|
+
require 'integration/test_helpers'
|
|
9
|
+
|
|
10
|
+
class TestGet < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
@store = IntegrationTestHelpers.build_store("get")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# get returns a found value for key
|
|
16
|
+
def test_get_returns_a_found_value_for_key
|
|
17
|
+
begin
|
|
18
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
19
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-test-key", {}, "my-test-value")
|
|
20
|
+
rescue Exception => e
|
|
21
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# get returns nil if value not found
|
|
26
|
+
def test_get_returns_nil_if_value_not_found
|
|
27
|
+
begin
|
|
28
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
29
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-missing-key", {}, nil)
|
|
30
|
+
rescue Exception => e
|
|
31
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# get returns a default for a missing value if a default is given
|
|
36
|
+
def test_get_returns_a_default_for_a_missing_value_if_a_default_is_given
|
|
37
|
+
begin
|
|
38
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
39
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-missing-key", {}, "DEFAULT")
|
|
40
|
+
rescue Exception => e
|
|
41
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# get ignores a provided default if the key is found
|
|
46
|
+
def test_get_ignores_a_provided_default_if_the_key_is_found
|
|
47
|
+
begin
|
|
48
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
49
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-test-key", {}, "my-test-value")
|
|
50
|
+
rescue Exception => e
|
|
51
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# get can return a double
|
|
56
|
+
def test_get_can_return_a_double
|
|
57
|
+
begin
|
|
58
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
59
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-double-key", {}, 9.95)
|
|
60
|
+
rescue Exception => e
|
|
61
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# get can return a string list
|
|
66
|
+
def test_get_can_return_a_string_list
|
|
67
|
+
begin
|
|
68
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
69
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-string-list-key", {}, ["a", "b", "c"])
|
|
70
|
+
rescue Exception => e
|
|
71
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# can return an override based on the default context
|
|
76
|
+
def test_can_return_an_override_based_on_the_default_context
|
|
77
|
+
begin
|
|
78
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
79
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-overridden-key", {}, "overridden")
|
|
80
|
+
rescue Exception => e
|
|
81
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# can return a value provided by an environment variable
|
|
86
|
+
def test_can_return_a_value_provided_by_an_environment_variable
|
|
87
|
+
begin
|
|
88
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
89
|
+
IntegrationTestHelpers.assert_resolved(resolver, "prefab.secrets.encryption.key", {}, "c87ba22d8662282abe8a0e4651327b579cb64a454ab0f4c170b45b15f049a221")
|
|
90
|
+
rescue Exception => e
|
|
91
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# can return a value provided by an environment variable after type coercion
|
|
96
|
+
def test_can_return_a_value_provided_by_an_environment_variable_after_type_coercion
|
|
97
|
+
begin
|
|
98
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
99
|
+
IntegrationTestHelpers.assert_resolved(resolver, "provided.a.number", {}, 1234)
|
|
100
|
+
rescue Exception => e
|
|
101
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# can decrypt and return a secret value (with decryption key in in env var)
|
|
106
|
+
def test_can_decrypt_and_return_a_secret_value_with_decryption_key_in_in_env_var
|
|
107
|
+
begin
|
|
108
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
109
|
+
IntegrationTestHelpers.assert_resolved(resolver, "a.secret.config", {}, "hello.world")
|
|
110
|
+
rescue Exception => e
|
|
111
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# duration 200 ms
|
|
116
|
+
def test_duration_200_ms
|
|
117
|
+
begin
|
|
118
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
119
|
+
IntegrationTestHelpers.assert_resolved(resolver, "test.duration.PT0.2S", {}, 200)
|
|
120
|
+
rescue Exception => e
|
|
121
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# duration 90S
|
|
126
|
+
def test_duration_90s
|
|
127
|
+
begin
|
|
128
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
129
|
+
IntegrationTestHelpers.assert_resolved(resolver, "test.duration.PT90S", {}, 90000)
|
|
130
|
+
rescue Exception => e
|
|
131
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# duration 1.5M
|
|
136
|
+
def test_duration_1_5m
|
|
137
|
+
begin
|
|
138
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
139
|
+
IntegrationTestHelpers.assert_resolved(resolver, "test.duration.PT1.5M", {}, 90000)
|
|
140
|
+
rescue Exception => e
|
|
141
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# duration 0.5H
|
|
146
|
+
def test_duration_0_5h
|
|
147
|
+
begin
|
|
148
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
149
|
+
IntegrationTestHelpers.assert_resolved(resolver, "test.duration.PT0.5H", {}, 1800000)
|
|
150
|
+
rescue Exception => e
|
|
151
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# duration test.duration.P1DT6H2M1.5S
|
|
156
|
+
def test_duration_test_duration_p1dt6h2m1_5s
|
|
157
|
+
begin
|
|
158
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
159
|
+
IntegrationTestHelpers.assert_resolved(resolver, "test.duration.P1DT6H2M1.5S", {}, 108121500)
|
|
160
|
+
rescue Exception => e
|
|
161
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# json test
|
|
166
|
+
def test_json_test
|
|
167
|
+
begin
|
|
168
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
169
|
+
IntegrationTestHelpers.assert_resolved(resolver, "test.json", {}, {"a" => 1, "b" => "c"})
|
|
170
|
+
rescue Exception => e
|
|
171
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# get returns a native json object (not a stringified payload)
|
|
176
|
+
def test_get_returns_a_native_json_object_not_a_stringified_payload
|
|
177
|
+
begin
|
|
178
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
179
|
+
IntegrationTestHelpers.assert_resolved(resolver, "test.json", {}, {"a" => 1, "b" => "c"})
|
|
180
|
+
rescue Exception => e
|
|
181
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# list on left side test (1)
|
|
186
|
+
def test_list_on_left_side_test_1
|
|
187
|
+
begin
|
|
188
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
189
|
+
IntegrationTestHelpers.assert_resolved(resolver, "left.hand.list.test", {"user" => {"name" => "james", "aka" => ["happy", "sleepy"]}}, "correct")
|
|
190
|
+
rescue Exception => e
|
|
191
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# list on left side test (2)
|
|
196
|
+
def test_list_on_left_side_test_2
|
|
197
|
+
begin
|
|
198
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
199
|
+
IntegrationTestHelpers.assert_resolved(resolver, "left.hand.list.test", {"user" => {"name" => "james", "aka" => ["a", "b"]}}, "default")
|
|
200
|
+
rescue Exception => e
|
|
201
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# list on left side test opposite (1)
|
|
206
|
+
def test_list_on_left_side_test_opposite_1
|
|
207
|
+
begin
|
|
208
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
209
|
+
IntegrationTestHelpers.assert_resolved(resolver, "left.hand.test.opposite", {"user" => {"name" => "james", "aka" => ["happy", "sleepy"]}}, "default")
|
|
210
|
+
rescue Exception => e
|
|
211
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# list on left side test (3)
|
|
216
|
+
def test_list_on_left_side_test_3
|
|
217
|
+
begin
|
|
218
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
219
|
+
IntegrationTestHelpers.assert_resolved(resolver, "left.hand.test.opposite", {"user" => {"name" => "james", "aka" => ["a", "b"]}}, "correct")
|
|
220
|
+
rescue Exception => e
|
|
221
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# AUTO-GENERATED from integration-test-data/tests/eval/get_feature_flag.yaml.
|
|
4
|
+
# Regenerate with `bundle exec ruby scripts/generate_integration_tests.rb`.
|
|
5
|
+
# Do NOT edit by hand — changes will be overwritten.
|
|
6
|
+
|
|
7
|
+
require 'test_helper'
|
|
8
|
+
require 'integration/test_helpers'
|
|
9
|
+
|
|
10
|
+
class TestGetFeatureFlag < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
@store = IntegrationTestHelpers.build_store("get_feature_flag")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# get returns the underlying value for a feature flag
|
|
16
|
+
def test_get_returns_the_underlying_value_for_a_feature_flag
|
|
17
|
+
begin
|
|
18
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
19
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.integer", {}, 3)
|
|
20
|
+
rescue Exception => e
|
|
21
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# get returns the underlying value for a feature flag that matches the highest precedent rule
|
|
26
|
+
def test_get_returns_the_underlying_value_for_a_feature_flag_that_matches_the_highest_precedent_rule
|
|
27
|
+
begin
|
|
28
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
29
|
+
IntegrationTestHelpers.assert_resolved(resolver, "feature-flag.integer", {"user" => {"key" => "michael"}}, 5)
|
|
30
|
+
rescue Exception => e
|
|
31
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# AUTO-GENERATED from integration-test-data/tests/eval/get_or_raise.yaml.
|
|
4
|
+
# Regenerate with `bundle exec ruby scripts/generate_integration_tests.rb`.
|
|
5
|
+
# Do NOT edit by hand — changes will be overwritten.
|
|
6
|
+
|
|
7
|
+
require 'test_helper'
|
|
8
|
+
require 'integration/test_helpers'
|
|
9
|
+
|
|
10
|
+
class TestGetOrRaise < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
@store = IntegrationTestHelpers.build_store("get_or_raise")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# get_or_raise can raise an error if value not found
|
|
16
|
+
def test_get_or_raise_can_raise_an_error_if_value_not_found
|
|
17
|
+
begin
|
|
18
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
19
|
+
ctx = Quonfig::Context.new({})
|
|
20
|
+
assert_raises(Quonfig::Errors::MissingDefaultError) { resolver.get("my-missing-key", ctx) }
|
|
21
|
+
rescue Minitest::Assertion => e
|
|
22
|
+
skip("resolver not yet raising Quonfig::Errors::MissingDefaultError: #{e.message}")
|
|
23
|
+
rescue Exception => e
|
|
24
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# get_or_raise returns a default value instead of raising
|
|
29
|
+
def test_get_or_raise_returns_a_default_value_instead_of_raising
|
|
30
|
+
begin
|
|
31
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
32
|
+
IntegrationTestHelpers.assert_resolved(resolver, "my-missing-key", {}, "DEFAULT")
|
|
33
|
+
rescue Exception => e
|
|
34
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# get_or_raise raises the correct error if it doesn't raise on init timeout
|
|
39
|
+
def test_get_or_raise_raises_the_correct_error_if_it_doesn_t_raise_on_init_timeout
|
|
40
|
+
begin
|
|
41
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
42
|
+
ctx = Quonfig::Context.new({})
|
|
43
|
+
assert_raises(Quonfig::Errors::MissingDefaultError) { resolver.get("any-key", ctx) }
|
|
44
|
+
rescue Minitest::Assertion => e
|
|
45
|
+
skip("resolver not yet raising Quonfig::Errors::MissingDefaultError: #{e.message}")
|
|
46
|
+
rescue Exception => e
|
|
47
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# get_or_raise can raise an error if the client does not initialize in time
|
|
52
|
+
def test_get_or_raise_can_raise_an_error_if_the_client_does_not_initialize_in_time
|
|
53
|
+
skip('initialization_timeout not tested')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# raises an error if a config is provided by a missing environment variable
|
|
57
|
+
def test_raises_an_error_if_a_config_is_provided_by_a_missing_environment_variable
|
|
58
|
+
begin
|
|
59
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
60
|
+
ctx = Quonfig::Context.new({})
|
|
61
|
+
assert_raises(Quonfig::Errors::MissingEnvVarError) { resolver.get("provided.by.missing.env.var", ctx) }
|
|
62
|
+
rescue Minitest::Assertion => e
|
|
63
|
+
skip("resolver not yet raising Quonfig::Errors::MissingEnvVarError: #{e.message}")
|
|
64
|
+
rescue Exception => e
|
|
65
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# raises an error if an env-var-provided config cannot be coerced to configured type
|
|
70
|
+
def test_raises_an_error_if_an_env_var_provided_config_cannot_be_coerced_to_configured_type
|
|
71
|
+
begin
|
|
72
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
73
|
+
ctx = Quonfig::Context.new({})
|
|
74
|
+
assert_raises(Quonfig::Errors::EnvVarParseError) { resolver.get("provided.not.a.number", ctx) }
|
|
75
|
+
rescue Minitest::Assertion => e
|
|
76
|
+
skip("resolver not yet raising Quonfig::Errors::EnvVarParseError: #{e.message}")
|
|
77
|
+
rescue Exception => e
|
|
78
|
+
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# raises an error for decryption failure
|
|
83
|
+
def test_raises_an_error_for_decryption_failure
|
|
84
|
+
skip("raise-case (unable_to_decrypt) — no Quonfig::Errors mapping yet")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# AUTO-GENERATED from integration-test-data/tests/eval/get_weighted_values.yaml.
|
|
4
|
+
# Regenerate with `bundle exec ruby scripts/generate_integration_tests.rb`.
|
|
5
|
+
# Do NOT edit by hand — changes will be overwritten.
|
|
6
|
+
|
|
7
|
+
require 'test_helper'
|
|
8
|
+
require 'integration/test_helpers'
|
|
9
|
+
|
|
10
|
+
class TestGetWeightedValues < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
@store = IntegrationTestHelpers.build_store("get_weighted_values")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# weighted value is consistent 1
|
|
16
|
+
def test_weighted_value_is_consistent_1
|
|
17
|
+
skip("weighted resolver not yet ported to JSON criteria (qfg-dk6.x)")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# weighted value is consistent 2
|
|
21
|
+
def test_weighted_value_is_consistent_2
|
|
22
|
+
skip("weighted resolver not yet ported to JSON criteria (qfg-dk6.x)")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# weighted value is consistent 3
|
|
26
|
+
def test_weighted_value_is_consistent_3
|
|
27
|
+
skip("weighted resolver not yet ported to JSON criteria (qfg-dk6.x)")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'quonfig'
|
|
5
|
+
|
|
6
|
+
# Integration-test environment — the generated tests read these the same way
|
|
7
|
+
# the SDK does at runtime. Mirrors sdk-node/test/integration/setup.ts and
|
|
8
|
+
# sdk-go/internal/fixtures/test_helpers_test.go so behavior stays consistent
|
|
9
|
+
# across SDKs.
|
|
10
|
+
ENV['PREFAB_INTEGRATION_TEST_ENCRYPTION_KEY'] =
|
|
11
|
+
'c87ba22d8662282abe8a0e4651327b579cb64a454ab0f4c170b45b15f049a221'
|
|
12
|
+
ENV['IS_A_NUMBER'] = '1234'
|
|
13
|
+
ENV['NOT_A_NUMBER'] = 'not_a_number'
|
|
14
|
+
ENV.delete('MISSING_ENV_VAR')
|
|
15
|
+
|
|
16
|
+
# Shared fixture loader + resolver factory for the generated integration
|
|
17
|
+
# tests in sdk-ruby/test/integration/test_*.rb (qfg-dk6.23/.24). The evaluator
|
|
18
|
+
# wired up here still delegates to Quonfig::CriteriaEvaluator — once
|
|
19
|
+
# qfg-dk6.10 ports the criterion operators to the JSON Criterion type,
|
|
20
|
+
# generated tests will resolve end-to-end. Until then build_store simply
|
|
21
|
+
# parses the JSON fixtures into the ConfigStore.
|
|
22
|
+
module IntegrationTestHelpers
|
|
23
|
+
DATA_DIR = File.expand_path(
|
|
24
|
+
'../../../integration-test-data/data/integration-tests',
|
|
25
|
+
__dir__
|
|
26
|
+
)
|
|
27
|
+
ENV_ID = 'Production'
|
|
28
|
+
CONFIG_SUBDIRS = %w[configs feature-flags segments log-levels schemas].freeze
|
|
29
|
+
|
|
30
|
+
def self.data_dir
|
|
31
|
+
DATA_DIR
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# fixture_name matches the generator's YAML suite name (e.g. 'get',
|
|
35
|
+
# 'enabled'). Every suite shares the same config corpus — mirrors
|
|
36
|
+
# sdk-node/sdk-go, which also build a single store for the whole run —
|
|
37
|
+
# so the name is advisory. Accepting it keeps the call shape the task
|
|
38
|
+
# spec asks for and leaves room for per-suite overlays later.
|
|
39
|
+
def self.build_store(_fixture_name = nil)
|
|
40
|
+
unless Dir.exist?(DATA_DIR)
|
|
41
|
+
raise "[integration tests] fixtures not found at #{DATA_DIR} — " \
|
|
42
|
+
'clone quonfig/integration-test-data as a sibling of sdk-ruby.'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
store = Quonfig::ConfigStore.new
|
|
46
|
+
CONFIG_SUBDIRS.each do |subdir|
|
|
47
|
+
dir = File.join(DATA_DIR, subdir)
|
|
48
|
+
next unless Dir.exist?(dir)
|
|
49
|
+
|
|
50
|
+
Dir.glob(File.join(dir, '*.json')).each do |path|
|
|
51
|
+
raw = JSON.parse(File.read(path))
|
|
52
|
+
cfg = to_config_response(raw)
|
|
53
|
+
key = cfg[:key]
|
|
54
|
+
next if key.nil? || key.empty?
|
|
55
|
+
|
|
56
|
+
store.set(key, cfg)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
store
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.build_resolver(store)
|
|
63
|
+
evaluator = Quonfig::Evaluator.new(store, env_id: ENV_ID)
|
|
64
|
+
Quonfig::Resolver.new(store, evaluator)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Resolve +key+ against +context+ and assert the unwrapped value (and,
|
|
68
|
+
# when present, its reported value_type) match. Generated tests call
|
|
69
|
+
# this; keep the failure message specific so diffs are readable.
|
|
70
|
+
def self.assert_resolved(resolver, key, context, expected_value, expected_type = nil)
|
|
71
|
+
ctx = context.is_a?(Quonfig::Context) ? context : Quonfig::Context.new(context || {})
|
|
72
|
+
result = resolver.get(key, ctx)
|
|
73
|
+
raise Minitest::Assertion, "No evaluation returned for key #{key.inspect}" if result.nil?
|
|
74
|
+
|
|
75
|
+
actual = if result.respond_to?(:unwrapped_value)
|
|
76
|
+
result.unwrapped_value
|
|
77
|
+
elsif result.respond_to?(:value)
|
|
78
|
+
v = result.value
|
|
79
|
+
v.respond_to?(:string) ? v.string : v
|
|
80
|
+
else
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
unless actual == expected_value
|
|
85
|
+
raise Minitest::Assertion,
|
|
86
|
+
"#{key}: expected #{expected_value.inspect} (#{expected_type}), got #{actual.inspect}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if expected_type && result.respond_to?(:value_type)
|
|
90
|
+
unless result.value_type.to_s == expected_type.to_s
|
|
91
|
+
raise Minitest::Assertion,
|
|
92
|
+
"#{key}: expected type #{expected_type}, got #{result.value_type}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
actual
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Temporarily set env vars for the duration of the block and restore the
|
|
99
|
+
# originals (including absence) on exit — even if the block raises.
|
|
100
|
+
def self.with_env(vars_hash)
|
|
101
|
+
originals = {}
|
|
102
|
+
vars_hash.each do |k, v|
|
|
103
|
+
originals[k] = ENV[k]
|
|
104
|
+
ENV[k] = v
|
|
105
|
+
end
|
|
106
|
+
yield
|
|
107
|
+
ensure
|
|
108
|
+
originals.each do |k, v|
|
|
109
|
+
if v.nil?
|
|
110
|
+
ENV.delete(k)
|
|
111
|
+
else
|
|
112
|
+
ENV[k] = v
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Normalize the raw JSON config on disk into the shape the rest of the
|
|
118
|
+
# suite expects: one environment row for ENV_ID pulled out of the
|
|
119
|
+
# top-level `environments` array. Matches sdk-node/setup.ts:toConfigResponse.
|
|
120
|
+
def self.to_config_response(raw)
|
|
121
|
+
environment = nil
|
|
122
|
+
if raw['environments'].is_a?(Array)
|
|
123
|
+
match = raw['environments'].find { |e| e.is_a?(Hash) && e['id'] == ENV_ID }
|
|
124
|
+
environment = match if match
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
id: raw['id'] || '',
|
|
129
|
+
key: raw['key'],
|
|
130
|
+
type: raw['type'],
|
|
131
|
+
value_type: raw['valueType'],
|
|
132
|
+
send_to_client_sdk: raw['sendToClientSdk'] || false,
|
|
133
|
+
default: raw['default'] || { 'rules' => [] },
|
|
134
|
+
environment: environment,
|
|
135
|
+
raw: raw
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
private_class_method :to_config_response
|
|
139
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'integration/test_helpers'
|
|
5
|
+
|
|
6
|
+
# Verifies the shared helper that generated integration tests (qfg-dk6.23/.24)
|
|
7
|
+
# depend on: fixture loading, resolver construction, env-var scoping,
|
|
8
|
+
# and the assertion helper.
|
|
9
|
+
class TestIntegrationHelpers < Minitest::Test
|
|
10
|
+
def test_data_dir_is_the_integration_tests_sibling_repo
|
|
11
|
+
assert_equal 'integration-tests', File.basename(IntegrationTestHelpers.data_dir)
|
|
12
|
+
assert Dir.exist?(IntegrationTestHelpers.data_dir),
|
|
13
|
+
"integration-test-data sibling repo must exist at #{IntegrationTestHelpers.data_dir}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_build_store_loads_configs_from_subdirs
|
|
17
|
+
store = IntegrationTestHelpers.build_store('get')
|
|
18
|
+
|
|
19
|
+
assert_kind_of Quonfig::ConfigStore, store
|
|
20
|
+
refute_empty store.keys, 'build_store should load at least one config'
|
|
21
|
+
assert store.keys.include?('my-test-key'),
|
|
22
|
+
"expected 'my-test-key' in store keys (got #{store.keys.first(5).inspect}...)"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_build_resolver_wires_store_and_evaluator
|
|
26
|
+
store = IntegrationTestHelpers.build_store('get')
|
|
27
|
+
resolver = IntegrationTestHelpers.build_resolver(store)
|
|
28
|
+
|
|
29
|
+
assert_kind_of Quonfig::Resolver, resolver
|
|
30
|
+
assert_same store, resolver.store
|
|
31
|
+
assert_kind_of Quonfig::Evaluator, resolver.evaluator
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_env_vars_for_encryption_and_env_lookups_are_set_at_load
|
|
35
|
+
assert_equal 'c87ba22d8662282abe8a0e4651327b579cb64a454ab0f4c170b45b15f049a221',
|
|
36
|
+
ENV['PREFAB_INTEGRATION_TEST_ENCRYPTION_KEY']
|
|
37
|
+
# IS_A_NUMBER / NOT_A_NUMBER support the env-var lookup integration tests.
|
|
38
|
+
assert_equal '1234', ENV['IS_A_NUMBER']
|
|
39
|
+
assert_equal 'not_a_number', ENV['NOT_A_NUMBER']
|
|
40
|
+
assert_nil ENV['MISSING_ENV_VAR']
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_with_env_sets_and_restores
|
|
44
|
+
ENV['ORIGINAL_PRESENT'] = 'keep-me'
|
|
45
|
+
ENV.delete('ORIGINAL_ABSENT')
|
|
46
|
+
|
|
47
|
+
IntegrationTestHelpers.with_env(
|
|
48
|
+
'ORIGINAL_PRESENT' => 'overridden',
|
|
49
|
+
'ORIGINAL_ABSENT' => 'temporary'
|
|
50
|
+
) do
|
|
51
|
+
assert_equal 'overridden', ENV['ORIGINAL_PRESENT']
|
|
52
|
+
assert_equal 'temporary', ENV['ORIGINAL_ABSENT']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
assert_equal 'keep-me', ENV['ORIGINAL_PRESENT']
|
|
56
|
+
assert_nil ENV['ORIGINAL_ABSENT']
|
|
57
|
+
ensure
|
|
58
|
+
ENV.delete('ORIGINAL_PRESENT')
|
|
59
|
+
ENV.delete('ORIGINAL_ABSENT')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_with_env_restores_after_exception
|
|
63
|
+
ENV.delete('ROLLBACK_ME')
|
|
64
|
+
begin
|
|
65
|
+
IntegrationTestHelpers.with_env('ROLLBACK_ME' => 'set') do
|
|
66
|
+
raise 'boom'
|
|
67
|
+
end
|
|
68
|
+
rescue RuntimeError
|
|
69
|
+
# swallow — we only care that ENV was cleaned up
|
|
70
|
+
end
|
|
71
|
+
assert_nil ENV['ROLLBACK_ME']
|
|
72
|
+
end
|
|
73
|
+
end
|