launchdarkly-server-sdk 5.8.2 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +28 -122
  3. data/.ldrelease/circleci/linux/execute.sh +18 -0
  4. data/.ldrelease/circleci/mac/execute.sh +18 -0
  5. data/.ldrelease/circleci/template/build.sh +29 -0
  6. data/.ldrelease/circleci/template/publish.sh +23 -0
  7. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  8. data/.ldrelease/circleci/template/test.sh +10 -0
  9. data/.ldrelease/circleci/template/update-version.sh +8 -0
  10. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  11. data/.ldrelease/config.yml +7 -3
  12. data/CHANGELOG.md +9 -0
  13. data/CONTRIBUTING.md +1 -1
  14. data/Gemfile.lock +69 -42
  15. data/README.md +2 -2
  16. data/azure-pipelines.yml +1 -1
  17. data/launchdarkly-server-sdk.gemspec +16 -16
  18. data/lib/ldclient-rb.rb +0 -1
  19. data/lib/ldclient-rb/config.rb +15 -3
  20. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  21. data/lib/ldclient-rb/events.rb +1 -4
  22. data/lib/ldclient-rb/file_data_source.rb +1 -1
  23. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  24. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  25. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  26. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  27. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  28. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  29. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
  30. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  32. data/lib/ldclient-rb/ldclient.rb +14 -9
  33. data/lib/ldclient-rb/polling.rb +1 -4
  34. data/lib/ldclient-rb/requestor.rb +25 -15
  35. data/lib/ldclient-rb/stream.rb +9 -6
  36. data/lib/ldclient-rb/util.rb +12 -8
  37. data/lib/ldclient-rb/version.rb +1 -1
  38. data/spec/evaluation_detail_spec.rb +135 -0
  39. data/spec/event_sender_spec.rb +20 -2
  40. data/spec/http_util.rb +11 -1
  41. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  42. data/spec/impl/evaluator_clause_spec.rb +55 -0
  43. data/spec/impl/evaluator_operators_spec.rb +141 -0
  44. data/spec/impl/evaluator_rule_spec.rb +96 -0
  45. data/spec/impl/evaluator_segment_spec.rb +125 -0
  46. data/spec/impl/evaluator_spec.rb +305 -0
  47. data/spec/impl/evaluator_spec_base.rb +75 -0
  48. data/spec/impl/model/serialization_spec.rb +41 -0
  49. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  50. data/spec/ldclient_end_to_end_spec.rb +34 -0
  51. data/spec/ldclient_spec.rb +10 -8
  52. data/spec/polling_spec.rb +2 -2
  53. data/spec/redis_feature_store_spec.rb +2 -2
  54. data/spec/requestor_spec.rb +11 -11
  55. metadata +89 -46
  56. data/lib/ldclient-rb/evaluation.rb +0 -462
  57. data/spec/evaluation_spec.rb +0 -789
@@ -39,12 +39,29 @@ module LaunchDarkly
39
39
  "authorization" => [ sdk_key ],
40
40
  "content-type" => [ "application/json" ],
41
41
  "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
42
- "x-launchdarkly-event-schema" => [ "3" ]
42
+ "x-launchdarkly-event-schema" => [ "3" ],
43
+ "connection" => [ "Keep-Alive" ]
43
44
  })
44
45
  expect(req.header['x-launchdarkly-payload-id']).not_to eq []
45
46
  end
46
47
  end
47
-
48
+
49
+ it "can use a socket factory" do
50
+ with_server do |server|
51
+ server.setup_ok_response("/bulk", "")
52
+
53
+ config = Config.new(events_uri: "http://events.com/bulk", socket_factory: SocketFactoryFromHash.new({"events.com" => server.port}), logger: $null_log)
54
+ es = subject.new(sdk_key, config, nil, 0.1)
55
+
56
+ result = es.send_event_data(fake_data, "", false)
57
+
58
+ expect(result.success).to be true
59
+ req = server.await_request
60
+ expect(req.body).to eq fake_data
61
+ expect(req.host).to eq "events.com"
62
+ end
63
+ end
64
+
48
65
  it "generates a new payload ID for each payload" do
49
66
  with_sender_and_server do |es, server|
50
67
  server.setup_ok_response("/bulk", "")
@@ -78,6 +95,7 @@ module LaunchDarkly
78
95
  "authorization" => [ sdk_key ],
79
96
  "content-type" => [ "application/json" ],
80
97
  "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
98
+ "connection" => [ "Keep-Alive" ]
81
99
  })
82
100
  expect(req.header['x-launchdarkly-event-schema']).to eq []
83
101
  expect(req.header['x-launchdarkly-payload-id']).to eq []
@@ -3,7 +3,7 @@ require "webrick/httpproxy"
3
3
  require "webrick/https"
4
4
 
5
5
  class StubHTTPServer
6
- attr_reader :requests
6
+ attr_reader :requests, :port
7
7
 
8
8
  @@next_port = 50000
9
9
 
@@ -120,3 +120,13 @@ def with_server(server = nil)
120
120
  server.stop
121
121
  end
122
122
  end
123
+
124
+ class SocketFactoryFromHash
125
+ def initialize(ports = {})
126
+ @ports = ports
127
+ end
128
+
129
+ def open(uri, timeout)
130
+ TCPSocket.new 'localhost', @ports[uri]
131
+ end
132
+ end
@@ -0,0 +1,111 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::Impl::EvaluatorBucketing do
4
+ subject { LaunchDarkly::Impl::EvaluatorBucketing }
5
+
6
+ describe "bucket_user" do
7
+ it "gets expected bucket values for specific keys" do
8
+ user = { key: "userKeyA" }
9
+ bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
10
+ expect(bucket).to be_within(0.0000001).of(0.42157587);
11
+
12
+ user = { key: "userKeyB" }
13
+ bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
14
+ expect(bucket).to be_within(0.0000001).of(0.6708485);
15
+
16
+ user = { key: "userKeyC" }
17
+ bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
18
+ expect(bucket).to be_within(0.0000001).of(0.10343106);
19
+ end
20
+
21
+ it "can bucket by int value (equivalent to string)" do
22
+ user = {
23
+ key: "userkey",
24
+ custom: {
25
+ stringAttr: "33333",
26
+ intAttr: 33333
27
+ }
28
+ }
29
+ stringResult = subject.bucket_user(user, "hashKey", "stringAttr", "saltyA")
30
+ intResult = subject.bucket_user(user, "hashKey", "intAttr", "saltyA")
31
+
32
+ expect(intResult).to be_within(0.0000001).of(0.54771423)
33
+ expect(intResult).to eq(stringResult)
34
+ end
35
+
36
+ it "cannot bucket by float value" do
37
+ user = {
38
+ key: "userkey",
39
+ custom: {
40
+ floatAttr: 33.5
41
+ }
42
+ }
43
+ result = subject.bucket_user(user, "hashKey", "floatAttr", "saltyA")
44
+ expect(result).to eq(0.0)
45
+ end
46
+
47
+
48
+ it "cannot bucket by bool value" do
49
+ user = {
50
+ key: "userkey",
51
+ custom: {
52
+ boolAttr: true
53
+ }
54
+ }
55
+ result = subject.bucket_user(user, "hashKey", "boolAttr", "saltyA")
56
+ expect(result).to eq(0.0)
57
+ end
58
+ end
59
+
60
+ describe "variation_index_for_user" do
61
+ it "matches bucket" do
62
+ user = { key: "userkey" }
63
+ flag_key = "flagkey"
64
+ salt = "salt"
65
+
66
+ # First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
67
+ # so we can construct a rollout whose second bucket just barely contains that value
68
+ bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
69
+ expect(bucket_value).to be > 0
70
+ expect(bucket_value).to be < 100000
71
+
72
+ bad_variation_a = 0
73
+ matched_variation = 1
74
+ bad_variation_b = 2
75
+ rule = {
76
+ rollout: {
77
+ variations: [
78
+ { variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
79
+ { variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
80
+ { variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
81
+ ]
82
+ }
83
+ }
84
+ flag = { key: flag_key, salt: salt }
85
+
86
+ result_variation = subject.variation_index_for_user(flag, rule, user)
87
+ expect(result_variation).to be matched_variation
88
+ end
89
+
90
+ it "uses last bucket if bucket value is equal to total weight" do
91
+ user = { key: "userkey" }
92
+ flag_key = "flagkey"
93
+ salt = "salt"
94
+
95
+ bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
96
+
97
+ # We'll construct a list of variations that stops right at the target bucket value
98
+ rule = {
99
+ rollout: {
100
+ variations: [
101
+ { variation: 0, weight: bucket_value }
102
+ ]
103
+ }
104
+ }
105
+ flag = { key: flag_key, salt: salt }
106
+
107
+ result_variation = subject.variation_index_for_user(flag, rule, user)
108
+ expect(result_variation).to be 0
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,55 @@
1
+ require "spec_helper"
2
+ require "impl/evaluator_spec_base"
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ describe "Evaluator (clauses)", :evaluator_spec_base => true do
7
+ subject { Evaluator }
8
+
9
+ it "can match built-in attribute" do
10
+ user = { key: 'x', name: 'Bob' }
11
+ clause = { attribute: 'name', op: 'in', values: ['Bob'] }
12
+ flag = boolean_flag_with_clauses([clause])
13
+ expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true
14
+ end
15
+
16
+ it "can match custom attribute" do
17
+ user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
18
+ clause = { attribute: 'legs', op: 'in', values: [4] }
19
+ flag = boolean_flag_with_clauses([clause])
20
+ expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true
21
+ end
22
+
23
+ it "returns false for missing attribute" do
24
+ user = { key: 'x', name: 'Bob' }
25
+ clause = { attribute: 'legs', op: 'in', values: [4] }
26
+ flag = boolean_flag_with_clauses([clause])
27
+ expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false
28
+ end
29
+
30
+ it "returns false for unknown operator" do
31
+ user = { key: 'x', name: 'Bob' }
32
+ clause = { attribute: 'name', op: 'unknown', values: [4] }
33
+ flag = boolean_flag_with_clauses([clause])
34
+ expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false
35
+ end
36
+
37
+ it "does not stop evaluating rules after clause with unknown operator" do
38
+ user = { key: 'x', name: 'Bob' }
39
+ clause0 = { attribute: 'name', op: 'unknown', values: [4] }
40
+ rule0 = { clauses: [ clause0 ], variation: 1 }
41
+ clause1 = { attribute: 'name', op: 'in', values: ['Bob'] }
42
+ rule1 = { clauses: [ clause1 ], variation: 1 }
43
+ flag = boolean_flag_with_rules([rule0, rule1])
44
+ expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true
45
+ end
46
+
47
+ it "can be negated" do
48
+ user = { key: 'x', name: 'Bob' }
49
+ clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true }
50
+ flag = boolean_flag_with_clauses([clause])
51
+ expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,141 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::Impl::EvaluatorOperators do
4
+ subject { LaunchDarkly::Impl::EvaluatorOperators }
5
+
6
+ describe "operators" do
7
+ dateStr1 = "2017-12-06T00:00:00.000-07:00"
8
+ dateStr2 = "2017-12-06T00:01:01.000-07:00"
9
+ dateMs1 = 10000000
10
+ dateMs2 = 10000001
11
+ invalidDate = "hey what's this?"
12
+
13
+ operatorTests = [
14
+ # numeric comparisons
15
+ [ :in, 99, 99, true ],
16
+ [ :in, 99.0001, 99.0001, true ],
17
+ [ :in, 99, 99.0001, false ],
18
+ [ :in, 99.0001, 99, false ],
19
+ [ :lessThan, 99, 99.0001, true ],
20
+ [ :lessThan, 99.0001, 99, false ],
21
+ [ :lessThan, 99, 99, false ],
22
+ [ :lessThanOrEqual, 99, 99.0001, true ],
23
+ [ :lessThanOrEqual, 99.0001, 99, false ],
24
+ [ :lessThanOrEqual, 99, 99, true ],
25
+ [ :greaterThan, 99.0001, 99, true ],
26
+ [ :greaterThan, 99, 99.0001, false ],
27
+ [ :greaterThan, 99, 99, false ],
28
+ [ :greaterThanOrEqual, 99.0001, 99, true ],
29
+ [ :greaterThanOrEqual, 99, 99.0001, false ],
30
+ [ :greaterThanOrEqual, 99, 99, true ],
31
+
32
+ # string comparisons
33
+ [ :in, "x", "x", true ],
34
+ [ :in, "x", "xyz", false ],
35
+ [ :startsWith, "xyz", "x", true ],
36
+ [ :startsWith, "x", "xyz", false ],
37
+ [ :endsWith, "xyz", "z", true ],
38
+ [ :endsWith, "z", "xyz", false ],
39
+ [ :contains, "xyz", "y", true ],
40
+ [ :contains, "y", "xyz", false ],
41
+
42
+ # mixed strings and numbers
43
+ [ :in, "99", 99, false ],
44
+ [ :in, 99, "99", false ],
45
+ [ :contains, "99", 99, false ],
46
+ [ :startsWith, "99", 99, false ],
47
+ [ :endsWith, "99", 99, false ],
48
+ [ :lessThanOrEqual, "99", 99, false ],
49
+ [ :lessThanOrEqual, 99, "99", false ],
50
+ [ :greaterThanOrEqual, "99", 99, false ],
51
+ [ :greaterThanOrEqual, 99, "99", false ],
52
+
53
+ # regex
54
+ [ :matches, "hello world", "hello.*rld", true ],
55
+ [ :matches, "hello world", "hello.*orl", true ],
56
+ [ :matches, "hello world", "l+", true ],
57
+ [ :matches, "hello world", "(world|planet)", true ],
58
+ [ :matches, "hello world", "aloha", false ],
59
+ [ :matches, "hello world", "***not a regex", false ],
60
+
61
+ # dates
62
+ [ :before, dateStr1, dateStr2, true ],
63
+ [ :before, dateMs1, dateMs2, true ],
64
+ [ :before, dateStr2, dateStr1, false ],
65
+ [ :before, dateMs2, dateMs1, false ],
66
+ [ :before, dateStr1, dateStr1, false ],
67
+ [ :before, dateMs1, dateMs1, false ],
68
+ [ :before, dateStr1, invalidDate, false ],
69
+ [ :after, dateStr1, dateStr2, false ],
70
+ [ :after, dateMs1, dateMs2, false ],
71
+ [ :after, dateStr2, dateStr1, true ],
72
+ [ :after, dateMs2, dateMs1, true ],
73
+ [ :after, dateStr1, dateStr1, false ],
74
+ [ :after, dateMs1, dateMs1, false ],
75
+ [ :after, dateStr1, invalidDate, false ],
76
+
77
+ # semver
78
+ [ :semVerEqual, "2.0.1", "2.0.1", true ],
79
+ [ :semVerEqual, "2.0", "2.0.0", true ],
80
+ [ :semVerEqual, "2-rc1", "2.0.0-rc1", true ],
81
+ [ :semVerEqual, "2+build2", "2.0.0+build2", true ],
82
+ [ :semVerLessThan, "2.0.0", "2.0.1", true ],
83
+ [ :semVerLessThan, "2.0", "2.0.1", true ],
84
+ [ :semVerLessThan, "2.0.1", "2.0.0", false ],
85
+ [ :semVerLessThan, "2.0.1", "2.0", false ],
86
+ [ :semVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true ],
87
+ [ :semVerGreaterThan, "2.0.1", "2.0.0", true ],
88
+ [ :semVerGreaterThan, "2.0.1", "2.0", true ],
89
+ [ :semVerGreaterThan, "2.0.0", "2.0.1", false ],
90
+ [ :semVerGreaterThan, "2.0", "2.0.1", false ],
91
+ [ :semVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true ],
92
+ [ :semVerLessThan, "2.0.1", "xbad%ver", false ],
93
+ [ :semVerGreaterThan, "2.0.1", "xbad%ver", false ]
94
+ ]
95
+
96
+ operatorTests.each do |params|
97
+ op = params[0]
98
+ value1 = params[1]
99
+ value2 = params[2]
100
+ shouldBe = params[3]
101
+ it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
102
+ expect(subject::apply(op, value1, value2)).to be shouldBe
103
+ end
104
+ end
105
+ end
106
+
107
+ describe "user_value" do
108
+ [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous, :some_custom_attr].each do |attr|
109
+ it "returns nil if property #{attr} is not defined" do
110
+ expect(subject::user_value({}, attr)).to be nil
111
+ end
112
+ end
113
+
114
+ [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name].each do |attr|
115
+ it "gets string value of string property #{attr}" do
116
+ expect(subject::user_value({ attr => 'x' }, attr)).to eq 'x'
117
+ end
118
+
119
+ it "coerces non-string value of property #{attr} to string" do
120
+ expect(subject::user_value({ attr => 3 }, attr)).to eq '3'
121
+ end
122
+ end
123
+
124
+ it "gets boolean value of property anonymous" do
125
+ expect(subject::user_value({ anonymous: true }, :anonymous)).to be true
126
+ expect(subject::user_value({ anonymous: false }, :anonymous)).to be false
127
+ end
128
+
129
+ it "does not coerces non-boolean value of property anonymous" do
130
+ expect(subject::user_value({ anonymous: 3 }, :anonymous)).to eq 3
131
+ end
132
+
133
+ it "gets string value of custom property" do
134
+ expect(subject::user_value({ custom: { some_custom_attr: 'x' } }, :some_custom_attr)).to eq 'x'
135
+ end
136
+
137
+ it "gets non-string value of custom property" do
138
+ expect(subject::user_value({ custom: { some_custom_attr: 3 } }, :some_custom_attr)).to eq 3
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,96 @@
1
+ require "spec_helper"
2
+ require "impl/evaluator_spec_base"
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ describe "Evaluator (rules)", :evaluator_spec_base => true do
7
+ subject { Evaluator }
8
+
9
+ it "matches user from rules" do
10
+ rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 1 }
11
+ flag = boolean_flag_with_rules([rule])
12
+ user = { key: 'userkey' }
13
+ detail = EvaluationDetail.new(true, 1, EvaluationReason::rule_match(0, 'ruleid'))
14
+ result = basic_evaluator.evaluate(flag, user, factory)
15
+ expect(result.detail).to eq(detail)
16
+ expect(result.events).to eq(nil)
17
+ end
18
+
19
+ it "reuses rule match reason instances if possible" do
20
+ rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 1 }
21
+ flag = boolean_flag_with_rules([rule])
22
+ Model.postprocess_item_after_deserializing!(FEATURES, flag) # now there's a cached rule match reason
23
+ user = { key: 'userkey' }
24
+ detail = EvaluationDetail.new(true, 1, EvaluationReason::rule_match(0, 'ruleid'))
25
+ result1 = basic_evaluator.evaluate(flag, user, factory)
26
+ result2 = basic_evaluator.evaluate(flag, user, factory)
27
+ expect(result1.detail.reason.rule_id).to eq 'ruleid'
28
+ expect(result1.detail.reason).to be result2.detail.reason
29
+ end
30
+
31
+ it "returns an error if rule variation is too high" do
32
+ rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 999 }
33
+ flag = boolean_flag_with_rules([rule])
34
+ user = { key: 'userkey' }
35
+ detail = EvaluationDetail.new(nil, nil,
36
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
37
+ result = basic_evaluator.evaluate(flag, user, factory)
38
+ expect(result.detail).to eq(detail)
39
+ expect(result.events).to eq(nil)
40
+ end
41
+
42
+ it "returns an error if rule variation is negative" do
43
+ rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: -1 }
44
+ flag = boolean_flag_with_rules([rule])
45
+ user = { key: 'userkey' }
46
+ detail = EvaluationDetail.new(nil, nil,
47
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
48
+ result = basic_evaluator.evaluate(flag, user, factory)
49
+ expect(result.detail).to eq(detail)
50
+ expect(result.events).to eq(nil)
51
+ end
52
+
53
+ it "returns an error if rule has neither variation nor rollout" do
54
+ rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }] }
55
+ flag = boolean_flag_with_rules([rule])
56
+ user = { key: 'userkey' }
57
+ detail = EvaluationDetail.new(nil, nil,
58
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
59
+ result = basic_evaluator.evaluate(flag, user, factory)
60
+ expect(result.detail).to eq(detail)
61
+ expect(result.events).to eq(nil)
62
+ end
63
+
64
+ it "returns an error if rule has a rollout with no variations" do
65
+ rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
66
+ rollout: { variations: [] } }
67
+ flag = boolean_flag_with_rules([rule])
68
+ user = { key: 'userkey' }
69
+ detail = EvaluationDetail.new(nil, nil,
70
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
71
+ result = basic_evaluator.evaluate(flag, user, factory)
72
+ expect(result.detail).to eq(detail)
73
+ expect(result.events).to eq(nil)
74
+ end
75
+
76
+ it "coerces user key to a string for evaluation" do
77
+ clause = { attribute: 'key', op: 'in', values: ['999'] }
78
+ flag = boolean_flag_with_clauses([clause])
79
+ user = { key: 999 }
80
+ result = basic_evaluator.evaluate(flag, user, factory)
81
+ expect(result.detail.value).to eq(true)
82
+ end
83
+
84
+ it "coerces secondary key to a string for evaluation" do
85
+ # We can't really verify that the rollout calculation works correctly, but we can at least
86
+ # make sure it doesn't error out if there's a non-string secondary value (ch35189)
87
+ rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
88
+ rollout: { salt: '', variations: [ { weight: 100000, variation: 1 } ] } }
89
+ flag = boolean_flag_with_rules([rule])
90
+ user = { key: "userkey", secondary: 999 }
91
+ result = basic_evaluator.evaluate(flag, user, factory)
92
+ expect(result.detail.reason).to eq(EvaluationReason::rule_match(0, 'ruleid'))
93
+ end
94
+ end
95
+ end
96
+ end