launchdarkly-server-sdk 5.8.2 → 6.0.0

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 (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