prefab-cloud-ruby 1.8.8.pre.1 → 1.8.9
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/.github/pull_request_template.md +8 -0
- data/.github/workflows/ruby.yml +1 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +94 -81
- data/VERSION +1 -1
- data/compile_protos.sh +4 -2
- data/lib/prefab/caching_http_connection.rb +95 -0
- data/lib/prefab/config_client.rb +7 -8
- data/lib/prefab/context.rb +1 -8
- data/lib/prefab/criteria_evaluator.rb +182 -9
- data/lib/prefab/fixed_size_hash.rb +14 -0
- data/lib/prefab/http_connection.rb +10 -6
- data/lib/prefab/semver.rb +132 -0
- data/lib/prefab-cloud-ruby.rb +3 -0
- data/lib/prefab_pb.rb +4 -23
- data/prefab-cloud-ruby.gemspec +27 -39
- data/test/support/common_helpers.rb +17 -13
- data/test/test_caching_http_connection.rb +218 -0
- data/test/test_context.rb +0 -15
- data/test/test_context_shape.rb +1 -1
- data/test/test_criteria_evaluator.rb +333 -0
- data/test/test_evaluation_summary_aggregator.rb +69 -69
- data/test/test_fixed_size_hash.rb +119 -0
- data/test/test_semver.rb +108 -0
- metadata +12 -5
@@ -11,7 +11,6 @@ module CommonHelpers
|
|
11
11
|
Prefab::Context.default_context.clear
|
12
12
|
SemanticLogger.add_appender(io: $logs, filter: Prefab.log_filter)
|
13
13
|
SemanticLogger.sync!
|
14
|
-
Timecop.freeze('2023-08-09 15:18:12 -0400')
|
15
14
|
end
|
16
15
|
|
17
16
|
def teardown
|
@@ -26,14 +25,19 @@ module CommonHelpers
|
|
26
25
|
end
|
27
26
|
end
|
28
27
|
|
29
|
-
|
28
|
+
#note this skips the output check in environments like rubymine that hijack the output. Alternative is a method missing error on string
|
29
|
+
|
30
|
+
if $stderr != $oldstderr && $stderr.respond_to?(:string) && !$stderr.string.empty?
|
30
31
|
# we ignore 2.X because of the number of `instance variable @xyz not initialized` warnings
|
31
32
|
if !RUBY_VERSION.start_with?('2.')
|
32
33
|
raise "Unexpected stderr. Handle stderr with assert_stderr\n\n#{$stderr.string}"
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
36
|
-
|
37
|
+
# Only restore stderr if we have a valid oldstderr
|
38
|
+
if $oldstderr
|
39
|
+
$stderr = $oldstderr
|
40
|
+
end
|
37
41
|
|
38
42
|
Timecop.return
|
39
43
|
end
|
@@ -174,18 +178,18 @@ module CommonHelpers
|
|
174
178
|
end
|
175
179
|
|
176
180
|
def assert_stderr(expected)
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
181
|
+
skip "Cannot verify stderr in current environment" unless $stderr.respond_to?(:string)
|
182
|
+
$stderr.string.split("\n").uniq.each do |line|
|
183
|
+
matched = false
|
184
|
+
|
185
|
+
expected.reject! do |expectation|
|
186
|
+
matched = true if line.include?(expectation)
|
187
|
+
end
|
188
|
+
|
189
|
+
assert(matched, "expectation: #{expected}, got: #{line}")
|
186
190
|
end
|
187
191
|
|
188
|
-
|
192
|
+
assert expected.empty?, "Expected stderr to include: #{expected}, but it did not"
|
189
193
|
|
190
194
|
# restore since we've handled it
|
191
195
|
$stderr = $oldstderr
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module Prefab
|
6
|
+
class CachingHttpConnectionTest < Minitest::Test
|
7
|
+
def setup
|
8
|
+
@uri = 'https://api.example.com'
|
9
|
+
@api_key = 'test-key'
|
10
|
+
@path = '/some/path'
|
11
|
+
|
12
|
+
# Reset the cache before each test
|
13
|
+
CachingHttpConnection.reset_cache!
|
14
|
+
|
15
|
+
# Setup the mock HTTP connection
|
16
|
+
@http_connection = Minitest::Mock.new
|
17
|
+
@http_connection.expect(:uri, @uri)
|
18
|
+
|
19
|
+
# Stub the HttpConnection constructor
|
20
|
+
HttpConnection.stub :new, @http_connection do
|
21
|
+
@subject = CachingHttpConnection.new(@uri, @api_key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_caches_responses_with_etag_and_max_age
|
26
|
+
response_body = 'response data'
|
27
|
+
response = Faraday::Response.new(
|
28
|
+
status: 200,
|
29
|
+
body: response_body,
|
30
|
+
response_headers: {
|
31
|
+
'ETag' => 'abc123',
|
32
|
+
'Cache-Control' => 'max-age=60'
|
33
|
+
}
|
34
|
+
)
|
35
|
+
|
36
|
+
# Expect two calls to uri (one for each request) and one call to get
|
37
|
+
@http_connection.expect(:uri, @uri)
|
38
|
+
@http_connection.expect(:get, response, [@path])
|
39
|
+
|
40
|
+
HttpConnection.stub :new, @http_connection do
|
41
|
+
# First request should miss cache
|
42
|
+
first_response = @subject.get(@path)
|
43
|
+
assert_equal response_body, first_response.body
|
44
|
+
assert_equal 'MISS', first_response.headers['X-Cache']
|
45
|
+
|
46
|
+
# Second request should hit cache
|
47
|
+
second_response = @subject.get(@path)
|
48
|
+
assert_equal response_body, second_response.body
|
49
|
+
assert_equal 'HIT', second_response.headers['X-Cache']
|
50
|
+
end
|
51
|
+
|
52
|
+
@http_connection.verify
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_respects_max_age_directive
|
56
|
+
response = Faraday::Response.new(
|
57
|
+
status: 200,
|
58
|
+
body: 'fresh data',
|
59
|
+
response_headers: {
|
60
|
+
'ETag' => 'abc123',
|
61
|
+
'Cache-Control' => 'max-age=60'
|
62
|
+
}
|
63
|
+
)
|
64
|
+
|
65
|
+
mock = Minitest::Mock.new
|
66
|
+
def mock.uri
|
67
|
+
'https://api.example.com'
|
68
|
+
end
|
69
|
+
|
70
|
+
# First request
|
71
|
+
mock.expect(:get, response, [@path])
|
72
|
+
# After max-age expires, new request with etag
|
73
|
+
mock.expect(:get, response, [@path, { 'If-None-Match' => 'abc123' }])
|
74
|
+
|
75
|
+
Timecop.freeze do
|
76
|
+
subject = CachingHttpConnection.new(@uri, @api_key)
|
77
|
+
subject.instance_variable_set('@connection', mock)
|
78
|
+
|
79
|
+
# Initial request
|
80
|
+
subject.get(@path)
|
81
|
+
|
82
|
+
# Within max-age window
|
83
|
+
Timecop.travel(59)
|
84
|
+
cached_response = subject.get(@path)
|
85
|
+
assert_equal 'HIT', cached_response.headers['X-Cache']
|
86
|
+
|
87
|
+
# After max-age window
|
88
|
+
Timecop.travel(61)
|
89
|
+
new_response = subject.get(@path)
|
90
|
+
assert_equal 'MISS', new_response.headers['X-Cache']
|
91
|
+
end
|
92
|
+
|
93
|
+
mock.verify
|
94
|
+
end
|
95
|
+
def test_handles_304_not_modified
|
96
|
+
initial_response = Faraday::Response.new(
|
97
|
+
status: 200,
|
98
|
+
body: 'cached data',
|
99
|
+
response_headers: { 'ETag' => 'abc123' }
|
100
|
+
)
|
101
|
+
|
102
|
+
not_modified_response = Faraday::Response.new(
|
103
|
+
status: 304,
|
104
|
+
body: '',
|
105
|
+
response_headers: { 'ETag' => 'abc123' }
|
106
|
+
)
|
107
|
+
|
108
|
+
mock = Minitest::Mock.new
|
109
|
+
def mock.uri
|
110
|
+
'https://api.example.com'
|
111
|
+
end
|
112
|
+
|
113
|
+
# First request with single arg
|
114
|
+
mock.expect(:get, initial_response, [@path])
|
115
|
+
|
116
|
+
# Second request with both path and headers
|
117
|
+
mock.expect(:get, not_modified_response, [@path, { 'If-None-Match' => 'abc123' }])
|
118
|
+
|
119
|
+
subject = CachingHttpConnection.new(@uri, @api_key)
|
120
|
+
subject.instance_variable_set('@connection', mock)
|
121
|
+
|
122
|
+
# Initial request to populate cache
|
123
|
+
first_response = subject.get(@path)
|
124
|
+
assert_equal 'cached data', first_response.body
|
125
|
+
assert_equal 'MISS', first_response.headers['X-Cache']
|
126
|
+
|
127
|
+
# Subsequent request gets 304
|
128
|
+
cached_response = subject.get(@path)
|
129
|
+
assert_equal 'cached data', cached_response.body
|
130
|
+
assert_equal 200, cached_response.status
|
131
|
+
assert_equal 'HIT', cached_response.headers['X-Cache']
|
132
|
+
|
133
|
+
mock.verify
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_does_not_cache_no_store_responses
|
137
|
+
response = Faraday::Response.new(
|
138
|
+
status: 200,
|
139
|
+
body: 'uncacheable data',
|
140
|
+
response_headers: { 'Cache-Control' => 'no-store' }
|
141
|
+
)
|
142
|
+
|
143
|
+
mock = Minitest::Mock.new
|
144
|
+
def mock.uri
|
145
|
+
'https://api.example.com'
|
146
|
+
end
|
147
|
+
# Both gets with single arg
|
148
|
+
mock.expect(:get, response, [@path])
|
149
|
+
mock.expect(:get, response, [@path])
|
150
|
+
|
151
|
+
subject = CachingHttpConnection.new(@uri, @api_key)
|
152
|
+
subject.instance_variable_set('@connection', mock)
|
153
|
+
|
154
|
+
2.times do
|
155
|
+
result = subject.get(@path)
|
156
|
+
assert_equal 'MISS', result.headers['X-Cache']
|
157
|
+
end
|
158
|
+
|
159
|
+
mock.verify
|
160
|
+
end
|
161
|
+
def test_cache_is_shared_across_instances
|
162
|
+
HttpConnection.stub :new, @http_connection do
|
163
|
+
instance1 = CachingHttpConnection.new(@uri, @api_key)
|
164
|
+
instance2 = CachingHttpConnection.new(@uri, @api_key)
|
165
|
+
|
166
|
+
assert_same instance1.class.cache, instance2.class.cache
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_cache_can_be_reset
|
171
|
+
old_cache = CachingHttpConnection.cache
|
172
|
+
CachingHttpConnection.reset_cache!
|
173
|
+
refute_same CachingHttpConnection.cache, old_cache
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_adds_if_none_match_header_when_cached
|
177
|
+
# First response to be cached
|
178
|
+
initial_response = Faraday::Response.new(
|
179
|
+
status: 200,
|
180
|
+
body: 'cached data',
|
181
|
+
response_headers: { 'ETag' => 'abc123' }
|
182
|
+
)
|
183
|
+
|
184
|
+
# Second request should have If-None-Match header
|
185
|
+
not_modified_response = Faraday::Response.new(
|
186
|
+
status: 304,
|
187
|
+
body: '',
|
188
|
+
response_headers: { 'ETag' => 'abc123' }
|
189
|
+
)
|
190
|
+
|
191
|
+
mock = Minitest::Mock.new
|
192
|
+
def mock.uri
|
193
|
+
'https://api.example.com'
|
194
|
+
end
|
195
|
+
|
196
|
+
# First request should not have If-None-Match
|
197
|
+
mock.expect(:get, initial_response, [@path])
|
198
|
+
|
199
|
+
# Second request should have If-None-Match header
|
200
|
+
mock.expect(:get, not_modified_response, [@path, { 'If-None-Match' => 'abc123' }])
|
201
|
+
|
202
|
+
subject = CachingHttpConnection.new(@uri, @api_key)
|
203
|
+
subject.instance_variable_set('@connection', mock)
|
204
|
+
|
205
|
+
# Initial request to populate cache
|
206
|
+
first_response = subject.get(@path)
|
207
|
+
assert_equal 'cached data', first_response.body
|
208
|
+
assert_equal 'MISS', first_response.headers['X-Cache']
|
209
|
+
|
210
|
+
# Second request should use If-None-Match
|
211
|
+
cached_response = subject.get(@path)
|
212
|
+
assert_equal 'cached data', cached_response.body
|
213
|
+
assert_equal 'HIT', cached_response.headers['X-Cache']
|
214
|
+
|
215
|
+
mock.verify
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
data/test/test_context.rb
CHANGED
@@ -141,14 +141,6 @@ class TestContext < Minitest::Test
|
|
141
141
|
"id" => PrefabProto::ConfigValue.new(int: 2),
|
142
142
|
"name" => PrefabProto::ConfigValue.new(string: "team-name")
|
143
143
|
}
|
144
|
-
),
|
145
|
-
|
146
|
-
PrefabProto::Context.new(
|
147
|
-
type: "prefab",
|
148
|
-
values: {
|
149
|
-
'current-time' => PrefabProto::ConfigValue.new(int: Prefab::TimeHelpers.now_in_ms),
|
150
|
-
'namespace' => PrefabProto::ConfigValue.new(string: namespace)
|
151
|
-
}
|
152
144
|
)
|
153
145
|
]
|
154
146
|
), contexts.to_proto(namespace)
|
@@ -226,13 +218,6 @@ class TestContext < Minitest::Test
|
|
226
218
|
"name" => PrefabProto::ConfigValue.new(string: "team-name")
|
227
219
|
}
|
228
220
|
),
|
229
|
-
# via to_proto
|
230
|
-
PrefabProto::Context.new(
|
231
|
-
type: "prefab",
|
232
|
-
values: {
|
233
|
-
'current-time' => PrefabProto::ConfigValue.new(int: Prefab::TimeHelpers.now_in_ms),
|
234
|
-
}
|
235
|
-
)
|
236
221
|
]
|
237
222
|
)
|
238
223
|
|
data/test/test_context_shape.rb
CHANGED
@@ -36,7 +36,7 @@ class TestContextShape < Minitest::Test
|
|
36
36
|
|
37
37
|
# If this test fails, it means that we've added a new type to the ConfigValue
|
38
38
|
def test_mapping_is_exhaustive
|
39
|
-
unsupported = [:bytes, :limit_definition, :log_level, :weighted_values, :int_range, :provided, :duration, :json]
|
39
|
+
unsupported = [:bytes, :limit_definition, :log_level, :weighted_values, :int_range, :provided, :duration, :json, :schema]
|
40
40
|
type_fields = PrefabProto::ConfigValue.descriptor.lookup_oneof("type").entries
|
41
41
|
supported = type_fields.entries.reject do |entry|
|
42
42
|
unsupported.include?(entry.name.to_sym)
|
@@ -808,6 +808,288 @@ class TestCriteriaEvaluator < Minitest::Test
|
|
808
808
|
assert_equal 'ghi', evaluator.evaluate(context).unwrapped_value
|
809
809
|
end
|
810
810
|
|
811
|
+
def test_prop_regex_matches
|
812
|
+
config = PrefabProto::Config.new(
|
813
|
+
key: KEY,
|
814
|
+
rows: [
|
815
|
+
DEFAULT_ROW,
|
816
|
+
PrefabProto::ConfigRow.new(
|
817
|
+
project_env_id: PROJECT_ENV_ID,
|
818
|
+
values: [
|
819
|
+
PrefabProto::ConditionalValue.new(
|
820
|
+
criteria: [
|
821
|
+
PrefabProto::Criterion.new(
|
822
|
+
operator: PrefabProto::Criterion::CriterionOperator::PROP_MATCHES,
|
823
|
+
value_to_match: PrefabProto::ConfigValue.new(string: "a+b+"),
|
824
|
+
property_name: 'user.testProperty'
|
825
|
+
)
|
826
|
+
],
|
827
|
+
value: DESIRED_VALUE_CONFIG
|
828
|
+
)
|
829
|
+
]
|
830
|
+
)
|
831
|
+
]
|
832
|
+
)
|
833
|
+
|
834
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
835
|
+
namespace: nil)
|
836
|
+
|
837
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
838
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: { testProperty: 'abc' })).unwrapped_value
|
839
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: { testProperty: 'aabb' })).unwrapped_value
|
840
|
+
end
|
841
|
+
|
842
|
+
def test_prop_regex_does_not_match
|
843
|
+
config = PrefabProto::Config.new(
|
844
|
+
key: KEY,
|
845
|
+
rows: [
|
846
|
+
DEFAULT_ROW,
|
847
|
+
PrefabProto::ConfigRow.new(
|
848
|
+
project_env_id: PROJECT_ENV_ID,
|
849
|
+
values: [
|
850
|
+
PrefabProto::ConditionalValue.new(
|
851
|
+
criteria: [
|
852
|
+
PrefabProto::Criterion.new(
|
853
|
+
operator: PrefabProto::Criterion::CriterionOperator::PROP_DOES_NOT_MATCH,
|
854
|
+
value_to_match: PrefabProto::ConfigValue.new(string: "a+b+"),
|
855
|
+
property_name: 'user.testProperty'
|
856
|
+
)
|
857
|
+
],
|
858
|
+
value: DESIRED_VALUE_CONFIG
|
859
|
+
)
|
860
|
+
]
|
861
|
+
)
|
862
|
+
]
|
863
|
+
)
|
864
|
+
|
865
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
866
|
+
namespace: nil)
|
867
|
+
|
868
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
869
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: { testProperty: 'abc' })).unwrapped_value
|
870
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: { testProperty: 'aabb' })).unwrapped_value
|
871
|
+
end
|
872
|
+
|
873
|
+
def test_prop_regex_does_not_match_returns_false_bad_regex
|
874
|
+
config = PrefabProto::Config.new(
|
875
|
+
key: KEY,
|
876
|
+
rows: [
|
877
|
+
DEFAULT_ROW,
|
878
|
+
PrefabProto::ConfigRow.new(
|
879
|
+
project_env_id: PROJECT_ENV_ID,
|
880
|
+
values: [
|
881
|
+
PrefabProto::ConditionalValue.new(
|
882
|
+
criteria: [
|
883
|
+
PrefabProto::Criterion.new(
|
884
|
+
operator: PrefabProto::Criterion::CriterionOperator::PROP_DOES_NOT_MATCH,
|
885
|
+
value_to_match: PrefabProto::ConfigValue.new(string: "[a+b+"),
|
886
|
+
property_name: 'user.testProperty'
|
887
|
+
)
|
888
|
+
],
|
889
|
+
value: DESIRED_VALUE_CONFIG
|
890
|
+
)
|
891
|
+
]
|
892
|
+
)
|
893
|
+
]
|
894
|
+
)
|
895
|
+
|
896
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
897
|
+
namespace: nil)
|
898
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
899
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: { testProperty: 'abc' })).unwrapped_value
|
900
|
+
|
901
|
+
assert_stderr [ "warning: character class has duplicated range: /^[a+b+$/" ]
|
902
|
+
end
|
903
|
+
|
904
|
+
|
905
|
+
def test_prop_regex_match_returns_false_bad_regex
|
906
|
+
config = PrefabProto::Config.new(
|
907
|
+
key: KEY,
|
908
|
+
rows: [
|
909
|
+
DEFAULT_ROW,
|
910
|
+
PrefabProto::ConfigRow.new(
|
911
|
+
project_env_id: PROJECT_ENV_ID,
|
912
|
+
values: [
|
913
|
+
PrefabProto::ConditionalValue.new(
|
914
|
+
criteria: [
|
915
|
+
PrefabProto::Criterion.new(
|
916
|
+
operator: PrefabProto::Criterion::CriterionOperator::PROP_MATCHES,
|
917
|
+
value_to_match: PrefabProto::ConfigValue.new(string: "[a+b+"),
|
918
|
+
property_name: 'user.testProperty'
|
919
|
+
)
|
920
|
+
],
|
921
|
+
value: DESIRED_VALUE_CONFIG
|
922
|
+
)
|
923
|
+
]
|
924
|
+
)
|
925
|
+
]
|
926
|
+
)
|
927
|
+
|
928
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
929
|
+
namespace: nil)
|
930
|
+
|
931
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
932
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: { testProperty: 'abc' })).unwrapped_value
|
933
|
+
|
934
|
+
assert_stderr [ "warning: character class has duplicated range: /^[a+b+$/" ]
|
935
|
+
end
|
936
|
+
|
937
|
+
def test_less_than_works
|
938
|
+
config = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_LESS_THAN, property_name: 'user.age', value_to_match: 10)
|
939
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
940
|
+
namespace: nil)
|
941
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
942
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 10})).unwrapped_value
|
943
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 9})).unwrapped_value
|
944
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 9.5})).unwrapped_value
|
945
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 10.1})).unwrapped_value
|
946
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "9"})).unwrapped_value
|
947
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "9.2"})).unwrapped_value
|
948
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "not a number"})).unwrapped_value
|
949
|
+
end
|
950
|
+
|
951
|
+
def test_less_or_equal_to_works
|
952
|
+
config = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_LESS_THAN_OR_EQUAL, property_name: 'user.age', value_to_match: 10)
|
953
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
954
|
+
namespace: nil)
|
955
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
956
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 10})).unwrapped_value
|
957
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 9})).unwrapped_value
|
958
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 9.5})).unwrapped_value
|
959
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 10.1})).unwrapped_value
|
960
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "9"})).unwrapped_value
|
961
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "not a number"})).unwrapped_value
|
962
|
+
end
|
963
|
+
|
964
|
+
|
965
|
+
def test_greater_than_works
|
966
|
+
config = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_GREATER_THAN, property_name: 'user.age', value_to_match: 10)
|
967
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
968
|
+
namespace: nil)
|
969
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
970
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 10})).unwrapped_value
|
971
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 9})).unwrapped_value
|
972
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 9.5})).unwrapped_value
|
973
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 10.1})).unwrapped_value
|
974
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 12})).unwrapped_value
|
975
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "19"})).unwrapped_value
|
976
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "not a number"})).unwrapped_value
|
977
|
+
end
|
978
|
+
|
979
|
+
def test_greater_than_or_equal_to_works
|
980
|
+
config = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_GREATER_THAN_OR_EQUAL, property_name: 'user.age', value_to_match: 10)
|
981
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
982
|
+
namespace: nil)
|
983
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
984
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 10})).unwrapped_value
|
985
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 9})).unwrapped_value
|
986
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: 9.5})).unwrapped_value
|
987
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 10.1})).unwrapped_value
|
988
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user: {age: 12})).unwrapped_value
|
989
|
+
|
990
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "19"})).unwrapped_value
|
991
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user: {age: "not a number"})).unwrapped_value
|
992
|
+
end
|
993
|
+
|
994
|
+
def test_date_before_works
|
995
|
+
date = "2024-12-01T00:00:00Z"
|
996
|
+
millis = Time.iso8601(date).utc.to_i * 1000
|
997
|
+
config_with_string = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_BEFORE, property_name: 'user.joinDate', value_to_match: date)
|
998
|
+
evaluator_with_string_config = Prefab::CriteriaEvaluator.new(config_with_string, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
999
|
+
namespace: nil)
|
1000
|
+
config_with_millis = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_BEFORE, property_name: 'user.joinDate', value_to_match: millis)
|
1001
|
+
evaluator_with_millis_config = Prefab::CriteriaEvaluator.new(config_with_millis, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
1002
|
+
namespace: nil)
|
1003
|
+
assert_equal DEFAULT_VALUE, evaluator_with_millis_config.evaluate(context({})).unwrapped_value
|
1004
|
+
assert_equal DESIRED_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: millis-10000})).unwrapped_value
|
1005
|
+
assert_equal DESIRED_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: "2024-11-01T00:00:00Z"})).unwrapped_value
|
1006
|
+
assert_equal DEFAULT_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: millis+10000})).unwrapped_value
|
1007
|
+
assert_equal DEFAULT_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: "2024-12-02T00:00:00Z"})).unwrapped_value
|
1008
|
+
|
1009
|
+
assert_equal DEFAULT_VALUE, evaluator_with_string_config.evaluate(context({})).unwrapped_value
|
1010
|
+
assert_equal DESIRED_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: millis-10000})).unwrapped_value
|
1011
|
+
assert_equal DESIRED_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: "2024-11-01T00:00:00Z"})).unwrapped_value
|
1012
|
+
assert_equal DEFAULT_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: millis+10000})).unwrapped_value
|
1013
|
+
assert_equal DEFAULT_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: "2024-12-02T00:00:00Z"})).unwrapped_value
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
def test_date_after_works
|
1017
|
+
date = "2024-12-01T00:00:00Z"
|
1018
|
+
millis = Time.iso8601(date).utc.to_i * 1000
|
1019
|
+
config_with_string = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_AFTER, property_name: 'user.joinDate', value_to_match: date)
|
1020
|
+
evaluator_with_string_config = Prefab::CriteriaEvaluator.new(config_with_string, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
1021
|
+
namespace: nil)
|
1022
|
+
config_with_millis = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_AFTER, property_name: 'user.joinDate', value_to_match: millis)
|
1023
|
+
evaluator_with_millis_config = Prefab::CriteriaEvaluator.new(config_with_millis, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client,
|
1024
|
+
namespace: nil)
|
1025
|
+
assert_equal DEFAULT_VALUE, evaluator_with_millis_config.evaluate(context({})).unwrapped_value
|
1026
|
+
assert_equal DEFAULT_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: millis-10000})).unwrapped_value
|
1027
|
+
assert_equal DEFAULT_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: "2024-11-01T00:00:00Z"})).unwrapped_value
|
1028
|
+
assert_equal DESIRED_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: millis+10000})).unwrapped_value
|
1029
|
+
assert_equal DESIRED_VALUE, evaluator_with_millis_config.evaluate(context(user: {joinDate: "2024-12-02T00:00:00Z"})).unwrapped_value
|
1030
|
+
|
1031
|
+
assert_equal DEFAULT_VALUE, evaluator_with_string_config.evaluate(context({})).unwrapped_value
|
1032
|
+
assert_equal DEFAULT_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: millis-10000})).unwrapped_value
|
1033
|
+
assert_equal DEFAULT_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: "2024-11-01T00:00:00Z"})).unwrapped_value
|
1034
|
+
assert_equal DESIRED_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: millis+10000})).unwrapped_value
|
1035
|
+
assert_equal DESIRED_VALUE, evaluator_with_string_config.evaluate(context(user: {joinDate: "2024-12-02T00:00:00Z"})).unwrapped_value
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
def test_semver_less_than
|
1039
|
+
config = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_SEMVER_LESS_THAN, property_name: 'user.version', value_to_match: "2.0.0")
|
1040
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client, namespace: nil)
|
1041
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
1042
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "nonsense"})).unwrapped_value
|
1043
|
+
|
1044
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user:{version: "1.0.0"})).unwrapped_value
|
1045
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "2.0.0"})).unwrapped_value
|
1046
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "3.0.0"})).unwrapped_value
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
def test_semver_equal_to
|
1050
|
+
config = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_SEMVER_EQUAL, property_name: 'user.version', value_to_match: "2.0.0")
|
1051
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client, namespace: nil)
|
1052
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
1053
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "nonsense"})).unwrapped_value
|
1054
|
+
|
1055
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "1.0.0"})).unwrapped_value
|
1056
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user:{version: "2.0.0"})).unwrapped_value
|
1057
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "3.0.0"})).unwrapped_value
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
def test_semver_greater_than
|
1061
|
+
config = create_prefab_config(operator: PrefabProto::Criterion::CriterionOperator::PROP_SEMVER_GREATER_THAN, property_name: 'user.version', value_to_match: "2.0.0")
|
1062
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client, namespace: nil)
|
1063
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
1064
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "nonsense"})).unwrapped_value
|
1065
|
+
|
1066
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "1.0.0"})).unwrapped_value
|
1067
|
+
assert_equal DEFAULT_VALUE, evaluator.evaluate(context(user:{version: "2.0.0"})).unwrapped_value
|
1068
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context(user:{version: "3.0.0"})).unwrapped_value
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
def test_date_before_with_current_time
|
1072
|
+
future_time = Time.now.utc + 3600 # 1 hour in the future
|
1073
|
+
config = create_prefab_config(
|
1074
|
+
operator: PrefabProto::Criterion::CriterionOperator::PROP_BEFORE,
|
1075
|
+
property_name: 'prefab.current-time',
|
1076
|
+
value_to_match: future_time.iso8601
|
1077
|
+
)
|
1078
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client, namespace: nil)
|
1079
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
1080
|
+
end
|
1081
|
+
|
1082
|
+
def test_date_after_with_current_time
|
1083
|
+
past_time = Time.now.utc - 3600 # 1 hour in the past
|
1084
|
+
config = create_prefab_config(
|
1085
|
+
operator: PrefabProto::Criterion::CriterionOperator::PROP_AFTER,
|
1086
|
+
property_name: 'prefab.current-time',
|
1087
|
+
value_to_match: past_time.iso8601
|
1088
|
+
)
|
1089
|
+
evaluator = Prefab::CriteriaEvaluator.new(config, project_env_id: PROJECT_ENV_ID, resolver: nil, base_client: @base_client, namespace: nil)
|
1090
|
+
assert_equal DESIRED_VALUE, evaluator.evaluate(context({})).unwrapped_value
|
1091
|
+
end
|
1092
|
+
|
811
1093
|
private
|
812
1094
|
|
813
1095
|
def string_list(values)
|
@@ -844,4 +1126,55 @@ class TestCriteriaEvaluator < Minitest::Test
|
|
844
1126
|
'fake-base-client-instance_hash'
|
845
1127
|
end
|
846
1128
|
end
|
1129
|
+
|
1130
|
+
|
1131
|
+
def create_prefab_config(
|
1132
|
+
key: KEY,
|
1133
|
+
project_env_id: PROJECT_ENV_ID,
|
1134
|
+
operator:,
|
1135
|
+
value_to_match:,
|
1136
|
+
property_name:,
|
1137
|
+
desired_value_config: DESIRED_VALUE_CONFIG
|
1138
|
+
)
|
1139
|
+
PrefabProto::Config.new(
|
1140
|
+
key: key,
|
1141
|
+
rows: [
|
1142
|
+
DEFAULT_ROW,
|
1143
|
+
PrefabProto::ConfigRow.new(
|
1144
|
+
project_env_id: project_env_id,
|
1145
|
+
values: [
|
1146
|
+
PrefabProto::ConditionalValue.new(
|
1147
|
+
criteria: [
|
1148
|
+
PrefabProto::Criterion.new(
|
1149
|
+
operator: operator,
|
1150
|
+
value_to_match: build_config_value(value_to_match),
|
1151
|
+
property_name: property_name
|
1152
|
+
)
|
1153
|
+
],
|
1154
|
+
value: desired_value_config
|
1155
|
+
)
|
1156
|
+
]
|
1157
|
+
)
|
1158
|
+
]
|
1159
|
+
)
|
1160
|
+
end
|
1161
|
+
|
1162
|
+
def build_config_value(value)
|
1163
|
+
case value
|
1164
|
+
when Integer
|
1165
|
+
PrefabProto::ConfigValue.new(int: value)
|
1166
|
+
when Float
|
1167
|
+
PrefabProto::ConfigValue.new(double: value)
|
1168
|
+
when String
|
1169
|
+
PrefabProto::ConfigValue.new(string: value)
|
1170
|
+
when Array
|
1171
|
+
if value.all? { |v| v.is_a?(String) }
|
1172
|
+
PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: value))
|
1173
|
+
else
|
1174
|
+
raise ArgumentError, "Unsupported array type: Only arrays of strings are supported."
|
1175
|
+
end
|
1176
|
+
else
|
1177
|
+
raise ArgumentError, "Unsupported value type: #{value.class}"
|
1178
|
+
end
|
1179
|
+
end
|
847
1180
|
end
|