launchdarkly-server-sdk 5.7.3 → 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 (70) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -122
  3. data/.gitignore +1 -1
  4. data/.ldrelease/build-docs.sh +18 -0
  5. data/.ldrelease/circleci/linux/execute.sh +18 -0
  6. data/.ldrelease/circleci/mac/execute.sh +18 -0
  7. data/.ldrelease/circleci/template/build.sh +29 -0
  8. data/.ldrelease/circleci/template/publish.sh +23 -0
  9. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  10. data/.ldrelease/circleci/template/test.sh +10 -0
  11. data/.ldrelease/circleci/template/update-version.sh +8 -0
  12. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  13. data/.ldrelease/config.yml +14 -2
  14. data/CHANGELOG.md +36 -0
  15. data/CONTRIBUTING.md +1 -1
  16. data/Gemfile.lock +92 -76
  17. data/README.md +5 -3
  18. data/azure-pipelines.yml +1 -1
  19. data/docs/Makefile +26 -0
  20. data/docs/index.md +9 -0
  21. data/launchdarkly-server-sdk.gemspec +20 -13
  22. data/lib/ldclient-rb.rb +0 -1
  23. data/lib/ldclient-rb/config.rb +15 -3
  24. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  25. data/lib/ldclient-rb/events.rb +1 -4
  26. data/lib/ldclient-rb/file_data_source.rb +1 -1
  27. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  28. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  29. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  30. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  31. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  32. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  33. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +8 -7
  34. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  35. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  36. data/lib/ldclient-rb/integrations/redis.rb +3 -0
  37. data/lib/ldclient-rb/ldclient.rb +16 -11
  38. data/lib/ldclient-rb/polling.rb +1 -4
  39. data/lib/ldclient-rb/redis_store.rb +1 -0
  40. data/lib/ldclient-rb/requestor.rb +25 -23
  41. data/lib/ldclient-rb/stream.rb +10 -30
  42. data/lib/ldclient-rb/user_filter.rb +3 -2
  43. data/lib/ldclient-rb/util.rb +12 -8
  44. data/lib/ldclient-rb/version.rb +1 -1
  45. data/spec/evaluation_detail_spec.rb +135 -0
  46. data/spec/event_sender_spec.rb +20 -2
  47. data/spec/events_spec.rb +11 -0
  48. data/spec/http_util.rb +11 -1
  49. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  50. data/spec/impl/evaluator_clause_spec.rb +55 -0
  51. data/spec/impl/evaluator_operators_spec.rb +141 -0
  52. data/spec/impl/evaluator_rule_spec.rb +96 -0
  53. data/spec/impl/evaluator_segment_spec.rb +125 -0
  54. data/spec/impl/evaluator_spec.rb +305 -0
  55. data/spec/impl/evaluator_spec_base.rb +75 -0
  56. data/spec/impl/model/serialization_spec.rb +41 -0
  57. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  58. data/spec/ldclient_end_to_end_spec.rb +34 -0
  59. data/spec/ldclient_spec.rb +10 -8
  60. data/spec/polling_spec.rb +2 -2
  61. data/spec/redis_feature_store_spec.rb +32 -3
  62. data/spec/requestor_spec.rb +11 -45
  63. data/spec/spec_helper.rb +0 -3
  64. data/spec/stream_spec.rb +1 -16
  65. metadata +110 -60
  66. data/.yardopts +0 -9
  67. data/lib/ldclient-rb/evaluation.rb +0 -462
  68. data/scripts/gendocs.sh +0 -11
  69. data/scripts/release.sh +0 -27
  70. data/spec/evaluation_spec.rb +0 -789
@@ -1,11 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Use this script to generate documentation locally in ./doc so it can be proofed before release.
4
- # After release, documentation will be visible at https://www.rubydoc.info/gems/launchdarkly-server-sdk
5
-
6
- gem install --conservative yard
7
- gem install --conservative redcarpet # provides Markdown formatting
8
-
9
- rm -rf doc/*
10
-
11
- yard doc
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env bash
2
- # This script updates the version for the launchdarkly-server-sdk library and releases it to RubyGems
3
- # It will only work if you have the proper credentials set up in ~/.gem/credentials
4
-
5
- # It takes exactly one argument: the new version.
6
- # It should be run from the root of this git repo like this:
7
- # ./scripts/release.sh 4.0.9
8
-
9
- # When done you should commit and push the changes made.
10
-
11
- set -uxe
12
- echo "Starting ruby-server-sdk release."
13
-
14
- VERSION=$1
15
-
16
- #Update version in lib/ldclient-rb/version.rb
17
- VERSION_RB_TEMP=./version.rb.tmp
18
- sed "s/VERSION =.*/VERSION = \"${VERSION}\"/g" lib/ldclient-rb/version.rb > ${VERSION_RB_TEMP}
19
- mv ${VERSION_RB_TEMP} lib/ldclient-rb/version.rb
20
-
21
- # Build Ruby Gem
22
- gem build launchdarkly-server-sdk.gemspec
23
-
24
- # Publish Ruby Gem
25
- gem push launchdarkly-server-sdk-${VERSION}.gem
26
-
27
- echo "Done with ruby-server-sdk release"
@@ -1,789 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe LaunchDarkly::Evaluation do
4
- subject { LaunchDarkly::Evaluation }
5
-
6
- include LaunchDarkly::Evaluation
7
-
8
- let(:features) { LaunchDarkly::InMemoryFeatureStore.new }
9
-
10
- let(:factory) { LaunchDarkly::Impl::EventFactory.new(false) }
11
-
12
- let(:user) {
13
- {
14
- key: "userkey",
15
- email: "test@example.com",
16
- name: "Bob"
17
- }
18
- }
19
-
20
- let(:logger) { $null_log }
21
-
22
- def boolean_flag_with_rules(rules)
23
- { key: 'feature', on: true, rules: rules, fallthrough: { variation: 0 }, variations: [ false, true ] }
24
- end
25
-
26
- def boolean_flag_with_clauses(clauses)
27
- boolean_flag_with_rules([{ id: 'ruleid', clauses: clauses, variation: 1 }])
28
- end
29
-
30
- describe "evaluate" do
31
- it "returns off variation if flag is off" do
32
- flag = {
33
- key: 'feature',
34
- on: false,
35
- offVariation: 1,
36
- fallthrough: { variation: 0 },
37
- variations: ['a', 'b', 'c']
38
- }
39
- user = { key: 'x' }
40
- detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'OFF' })
41
- result = evaluate(flag, user, features, logger, factory)
42
- expect(result.detail).to eq(detail)
43
- expect(result.events).to eq([])
44
- end
45
-
46
- it "returns nil if flag is off and off variation is unspecified" do
47
- flag = {
48
- key: 'feature',
49
- on: false,
50
- fallthrough: { variation: 0 },
51
- variations: ['a', 'b', 'c']
52
- }
53
- user = { key: 'x' }
54
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'OFF' })
55
- result = evaluate(flag, user, features, logger, factory)
56
- expect(result.detail).to eq(detail)
57
- expect(result.events).to eq([])
58
- end
59
-
60
- it "returns an error if off variation is too high" do
61
- flag = {
62
- key: 'feature',
63
- on: false,
64
- offVariation: 999,
65
- fallthrough: { variation: 0 },
66
- variations: ['a', 'b', 'c']
67
- }
68
- user = { key: 'x' }
69
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
70
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
71
- result = evaluate(flag, user, features, logger, factory)
72
- expect(result.detail).to eq(detail)
73
- expect(result.events).to eq([])
74
- end
75
-
76
- it "returns an error if off variation is negative" do
77
- flag = {
78
- key: 'feature',
79
- on: false,
80
- offVariation: -1,
81
- fallthrough: { variation: 0 },
82
- variations: ['a', 'b', 'c']
83
- }
84
- user = { key: 'x' }
85
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
86
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
87
- result = evaluate(flag, user, features, logger, factory)
88
- expect(result.detail).to eq(detail)
89
- expect(result.events).to eq([])
90
- end
91
-
92
- it "returns off variation if prerequisite is not found" do
93
- flag = {
94
- key: 'feature0',
95
- on: true,
96
- prerequisites: [{key: 'badfeature', variation: 1}],
97
- fallthrough: { variation: 0 },
98
- offVariation: 1,
99
- variations: ['a', 'b', 'c']
100
- }
101
- user = { key: 'x' }
102
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
103
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'badfeature' })
104
- result = evaluate(flag, user, features, logger, factory)
105
- expect(result.detail).to eq(detail)
106
- expect(result.events).to eq([])
107
- end
108
-
109
- it "returns off variation and event if prerequisite of a prerequisite is not found" do
110
- flag = {
111
- key: 'feature0',
112
- on: true,
113
- prerequisites: [{key: 'feature1', variation: 1}],
114
- fallthrough: { variation: 0 },
115
- offVariation: 1,
116
- variations: ['a', 'b', 'c'],
117
- version: 1
118
- }
119
- flag1 = {
120
- key: 'feature1',
121
- on: true,
122
- prerequisites: [{key: 'feature2', variation: 1}], # feature2 doesn't exist
123
- fallthrough: { variation: 0 },
124
- variations: ['d', 'e'],
125
- version: 2
126
- }
127
- features.upsert(LaunchDarkly::FEATURES, flag1)
128
- user = { key: 'x' }
129
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
130
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
131
- events_should_be = [{
132
- kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0'
133
- }]
134
- result = evaluate(flag, user, features, logger, factory)
135
- expect(result.detail).to eq(detail)
136
- expect(result.events).to eq(events_should_be)
137
- end
138
-
139
- it "returns off variation and event if prerequisite is off" do
140
- flag = {
141
- key: 'feature0',
142
- on: true,
143
- prerequisites: [{key: 'feature1', variation: 1}],
144
- fallthrough: { variation: 0 },
145
- offVariation: 1,
146
- variations: ['a', 'b', 'c'],
147
- version: 1
148
- }
149
- flag1 = {
150
- key: 'feature1',
151
- on: false,
152
- # note that even though it returns the desired variation, it is still off and therefore not a match
153
- offVariation: 1,
154
- fallthrough: { variation: 0 },
155
- variations: ['d', 'e'],
156
- version: 2
157
- }
158
- features.upsert(LaunchDarkly::FEATURES, flag1)
159
- user = { key: 'x' }
160
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
161
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
162
- events_should_be = [{
163
- kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
164
- }]
165
- result = evaluate(flag, user, features, logger, factory)
166
- expect(result.detail).to eq(detail)
167
- expect(result.events).to eq(events_should_be)
168
- end
169
-
170
- it "returns off variation and event if prerequisite is not met" do
171
- flag = {
172
- key: 'feature0',
173
- on: true,
174
- prerequisites: [{key: 'feature1', variation: 1}],
175
- fallthrough: { variation: 0 },
176
- offVariation: 1,
177
- variations: ['a', 'b', 'c'],
178
- version: 1
179
- }
180
- flag1 = {
181
- key: 'feature1',
182
- on: true,
183
- fallthrough: { variation: 0 },
184
- variations: ['d', 'e'],
185
- version: 2
186
- }
187
- features.upsert(LaunchDarkly::FEATURES, flag1)
188
- user = { key: 'x' }
189
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
190
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
191
- events_should_be = [{
192
- kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0'
193
- }]
194
- result = evaluate(flag, user, features, logger, factory)
195
- expect(result.detail).to eq(detail)
196
- expect(result.events).to eq(events_should_be)
197
- end
198
-
199
- it "returns fallthrough variation and event if prerequisite is met and there are no rules" do
200
- flag = {
201
- key: 'feature0',
202
- on: true,
203
- prerequisites: [{key: 'feature1', variation: 1}],
204
- fallthrough: { variation: 0 },
205
- offVariation: 1,
206
- variations: ['a', 'b', 'c'],
207
- version: 1
208
- }
209
- flag1 = {
210
- key: 'feature1',
211
- on: true,
212
- fallthrough: { variation: 1 },
213
- variations: ['d', 'e'],
214
- version: 2
215
- }
216
- features.upsert(LaunchDarkly::FEATURES, flag1)
217
- user = { key: 'x' }
218
- detail = LaunchDarkly::EvaluationDetail.new('a', 0, { kind: 'FALLTHROUGH' })
219
- events_should_be = [{
220
- kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
221
- }]
222
- result = evaluate(flag, user, features, logger, factory)
223
- expect(result.detail).to eq(detail)
224
- expect(result.events).to eq(events_should_be)
225
- end
226
-
227
- it "returns an error if fallthrough variation is too high" do
228
- flag = {
229
- key: 'feature',
230
- on: true,
231
- fallthrough: { variation: 999 },
232
- offVariation: 1,
233
- variations: ['a', 'b', 'c']
234
- }
235
- user = { key: 'userkey' }
236
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
237
- result = evaluate(flag, user, features, logger, factory)
238
- expect(result.detail).to eq(detail)
239
- expect(result.events).to eq([])
240
- end
241
-
242
- it "returns an error if fallthrough variation is negative" do
243
- flag = {
244
- key: 'feature',
245
- on: true,
246
- fallthrough: { variation: -1 },
247
- offVariation: 1,
248
- variations: ['a', 'b', 'c']
249
- }
250
- user = { key: 'userkey' }
251
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
252
- result = evaluate(flag, user, features, logger, factory)
253
- expect(result.detail).to eq(detail)
254
- expect(result.events).to eq([])
255
- end
256
-
257
- it "returns an error if fallthrough has no variation or rollout" do
258
- flag = {
259
- key: 'feature',
260
- on: true,
261
- fallthrough: { },
262
- offVariation: 1,
263
- variations: ['a', 'b', 'c']
264
- }
265
- user = { key: 'userkey' }
266
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
267
- result = evaluate(flag, user, features, logger, factory)
268
- expect(result.detail).to eq(detail)
269
- expect(result.events).to eq([])
270
- end
271
-
272
- it "returns an error if fallthrough has a rollout with no variations" do
273
- flag = {
274
- key: 'feature',
275
- on: true,
276
- fallthrough: { rollout: { variations: [] } },
277
- offVariation: 1,
278
- variations: ['a', 'b', 'c']
279
- }
280
- user = { key: 'userkey' }
281
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
282
- result = evaluate(flag, user, features, logger, factory)
283
- expect(result.detail).to eq(detail)
284
- expect(result.events).to eq([])
285
- end
286
-
287
- it "matches user from targets" do
288
- flag = {
289
- key: 'feature',
290
- on: true,
291
- targets: [
292
- { values: [ 'whoever', 'userkey' ], variation: 2 }
293
- ],
294
- fallthrough: { variation: 0 },
295
- offVariation: 1,
296
- variations: ['a', 'b', 'c']
297
- }
298
- user = { key: 'userkey' }
299
- detail = LaunchDarkly::EvaluationDetail.new('c', 2, { kind: 'TARGET_MATCH' })
300
- result = evaluate(flag, user, features, logger, factory)
301
- expect(result.detail).to eq(detail)
302
- expect(result.events).to eq([])
303
- end
304
-
305
- it "matches user from rules" do
306
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 1 }
307
- flag = boolean_flag_with_rules([rule])
308
- user = { key: 'userkey' }
309
- detail = LaunchDarkly::EvaluationDetail.new(true, 1,
310
- { kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid' })
311
- result = evaluate(flag, user, features, logger, factory)
312
- expect(result.detail).to eq(detail)
313
- expect(result.events).to eq([])
314
- end
315
-
316
- it "returns an error if rule variation is too high" do
317
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 999 }
318
- flag = boolean_flag_with_rules([rule])
319
- user = { key: 'userkey' }
320
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
321
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
322
- result = evaluate(flag, user, features, logger, factory)
323
- expect(result.detail).to eq(detail)
324
- expect(result.events).to eq([])
325
- end
326
-
327
- it "returns an error if rule variation is negative" do
328
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: -1 }
329
- flag = boolean_flag_with_rules([rule])
330
- user = { key: 'userkey' }
331
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
332
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
333
- result = evaluate(flag, user, features, logger, factory)
334
- expect(result.detail).to eq(detail)
335
- expect(result.events).to eq([])
336
- end
337
-
338
- it "returns an error if rule has neither variation nor rollout" do
339
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }] }
340
- flag = boolean_flag_with_rules([rule])
341
- user = { key: 'userkey' }
342
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
343
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
344
- result = evaluate(flag, user, features, logger, factory)
345
- expect(result.detail).to eq(detail)
346
- expect(result.events).to eq([])
347
- end
348
-
349
- it "returns an error if rule has a rollout with no variations" do
350
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
351
- rollout: { variations: [] } }
352
- flag = boolean_flag_with_rules([rule])
353
- user = { key: 'userkey' }
354
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
355
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
356
- result = evaluate(flag, user, features, logger, factory)
357
- expect(result.detail).to eq(detail)
358
- expect(result.events).to eq([])
359
- end
360
-
361
- it "coerces user key to a string for evaluation" do
362
- clause = { attribute: 'key', op: 'in', values: ['999'] }
363
- flag = boolean_flag_with_clauses([clause])
364
- user = { key: 999 }
365
- result = evaluate(flag, user, features, logger, factory)
366
- expect(result.detail.value).to eq(true)
367
- end
368
-
369
- it "coerces secondary key to a string for evaluation" do
370
- # We can't really verify that the rollout calculation works correctly, but we can at least
371
- # make sure it doesn't error out if there's a non-string secondary value (ch35189)
372
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
373
- rollout: { salt: '', variations: [ { weight: 100000, variation: 1 } ] } }
374
- flag = boolean_flag_with_rules([rule])
375
- user = { key: "userkey", secondary: 999 }
376
- result = evaluate(flag, user, features, logger, factory)
377
- expect(result.detail.reason).to eq({ kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid'})
378
- end
379
- end
380
-
381
- describe "clause" do
382
- it "can match built-in attribute" do
383
- user = { key: 'x', name: 'Bob' }
384
- clause = { attribute: 'name', op: 'in', values: ['Bob'] }
385
- flag = boolean_flag_with_clauses([clause])
386
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
387
- end
388
-
389
- it "can match custom attribute" do
390
- user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
391
- clause = { attribute: 'legs', op: 'in', values: [4] }
392
- flag = boolean_flag_with_clauses([clause])
393
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
394
- end
395
-
396
- it "returns false for missing attribute" do
397
- user = { key: 'x', name: 'Bob' }
398
- clause = { attribute: 'legs', op: 'in', values: [4] }
399
- flag = boolean_flag_with_clauses([clause])
400
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
401
- end
402
-
403
- it "returns false for unknown operator" do
404
- user = { key: 'x', name: 'Bob' }
405
- clause = { attribute: 'name', op: 'unknown', values: [4] }
406
- flag = boolean_flag_with_clauses([clause])
407
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
408
- end
409
-
410
- it "does not stop evaluating rules after clause with unknown operator" do
411
- user = { key: 'x', name: 'Bob' }
412
- clause0 = { attribute: 'name', op: 'unknown', values: [4] }
413
- rule0 = { clauses: [ clause0 ], variation: 1 }
414
- clause1 = { attribute: 'name', op: 'in', values: ['Bob'] }
415
- rule1 = { clauses: [ clause1 ], variation: 1 }
416
- flag = boolean_flag_with_rules([rule0, rule1])
417
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
418
- end
419
-
420
- it "can be negated" do
421
- user = { key: 'x', name: 'Bob' }
422
- clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true }
423
- flag = boolean_flag_with_clauses([clause])
424
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
425
- end
426
-
427
- it "retrieves segment from segment store for segmentMatch operator" do
428
- segment = {
429
- key: 'segkey',
430
- included: [ 'userkey' ],
431
- version: 1,
432
- deleted: false
433
- }
434
- features.upsert(LaunchDarkly::SEGMENTS, segment)
435
-
436
- user = { key: 'userkey' }
437
- clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
438
- flag = boolean_flag_with_clauses([clause])
439
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
440
- end
441
-
442
- it "falls through with no errors if referenced segment is not found" do
443
- user = { key: 'userkey' }
444
- clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
445
- flag = boolean_flag_with_clauses([clause])
446
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
447
- end
448
-
449
- it "can be negated" do
450
- user = { key: 'x', name: 'Bob' }
451
- clause = { attribute: 'name', op: 'in', values: ['Bob'] }
452
- flag = boolean_flag_with_clauses([clause])
453
- expect {
454
- clause[:negate] = true
455
- }.to change {evaluate(flag, user, features, logger, factory).detail.value}.from(true).to(false)
456
- end
457
- end
458
-
459
- describe "operators" do
460
- dateStr1 = "2017-12-06T00:00:00.000-07:00"
461
- dateStr2 = "2017-12-06T00:01:01.000-07:00"
462
- dateMs1 = 10000000
463
- dateMs2 = 10000001
464
- invalidDate = "hey what's this?"
465
-
466
- operatorTests = [
467
- # numeric comparisons
468
- [ :in, 99, 99, true ],
469
- [ :in, 99.0001, 99.0001, true ],
470
- [ :in, 99, 99.0001, false ],
471
- [ :in, 99.0001, 99, false ],
472
- [ :lessThan, 99, 99.0001, true ],
473
- [ :lessThan, 99.0001, 99, false ],
474
- [ :lessThan, 99, 99, false ],
475
- [ :lessThanOrEqual, 99, 99.0001, true ],
476
- [ :lessThanOrEqual, 99.0001, 99, false ],
477
- [ :lessThanOrEqual, 99, 99, true ],
478
- [ :greaterThan, 99.0001, 99, true ],
479
- [ :greaterThan, 99, 99.0001, false ],
480
- [ :greaterThan, 99, 99, false ],
481
- [ :greaterThanOrEqual, 99.0001, 99, true ],
482
- [ :greaterThanOrEqual, 99, 99.0001, false ],
483
- [ :greaterThanOrEqual, 99, 99, true ],
484
-
485
- # string comparisons
486
- [ :in, "x", "x", true ],
487
- [ :in, "x", "xyz", false ],
488
- [ :startsWith, "xyz", "x", true ],
489
- [ :startsWith, "x", "xyz", false ],
490
- [ :endsWith, "xyz", "z", true ],
491
- [ :endsWith, "z", "xyz", false ],
492
- [ :contains, "xyz", "y", true ],
493
- [ :contains, "y", "xyz", false ],
494
-
495
- # mixed strings and numbers
496
- [ :in, "99", 99, false ],
497
- [ :in, 99, "99", false ],
498
- [ :contains, "99", 99, false ],
499
- [ :startsWith, "99", 99, false ],
500
- [ :endsWith, "99", 99, false ],
501
- [ :lessThanOrEqual, "99", 99, false ],
502
- [ :lessThanOrEqual, 99, "99", false ],
503
- [ :greaterThanOrEqual, "99", 99, false ],
504
- [ :greaterThanOrEqual, 99, "99", false ],
505
-
506
- # regex
507
- [ :matches, "hello world", "hello.*rld", true ],
508
- [ :matches, "hello world", "hello.*orl", true ],
509
- [ :matches, "hello world", "l+", true ],
510
- [ :matches, "hello world", "(world|planet)", true ],
511
- [ :matches, "hello world", "aloha", false ],
512
- [ :matches, "hello world", "***not a regex", false ],
513
-
514
- # dates
515
- [ :before, dateStr1, dateStr2, true ],
516
- [ :before, dateMs1, dateMs2, true ],
517
- [ :before, dateStr2, dateStr1, false ],
518
- [ :before, dateMs2, dateMs1, false ],
519
- [ :before, dateStr1, dateStr1, false ],
520
- [ :before, dateMs1, dateMs1, false ],
521
- [ :before, dateStr1, invalidDate, false ],
522
- [ :after, dateStr1, dateStr2, false ],
523
- [ :after, dateMs1, dateMs2, false ],
524
- [ :after, dateStr2, dateStr1, true ],
525
- [ :after, dateMs2, dateMs1, true ],
526
- [ :after, dateStr1, dateStr1, false ],
527
- [ :after, dateMs1, dateMs1, false ],
528
- [ :after, dateStr1, invalidDate, false ],
529
-
530
- # semver
531
- [ :semVerEqual, "2.0.1", "2.0.1", true ],
532
- [ :semVerEqual, "2.0", "2.0.0", true ],
533
- [ :semVerEqual, "2-rc1", "2.0.0-rc1", true ],
534
- [ :semVerEqual, "2+build2", "2.0.0+build2", true ],
535
- [ :semVerLessThan, "2.0.0", "2.0.1", true ],
536
- [ :semVerLessThan, "2.0", "2.0.1", true ],
537
- [ :semVerLessThan, "2.0.1", "2.0.0", false ],
538
- [ :semVerLessThan, "2.0.1", "2.0", false ],
539
- [ :semVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true ],
540
- [ :semVerGreaterThan, "2.0.1", "2.0.0", true ],
541
- [ :semVerGreaterThan, "2.0.1", "2.0", true ],
542
- [ :semVerGreaterThan, "2.0.0", "2.0.1", false ],
543
- [ :semVerGreaterThan, "2.0", "2.0.1", false ],
544
- [ :semVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true ],
545
- [ :semVerLessThan, "2.0.1", "xbad%ver", false ],
546
- [ :semVerGreaterThan, "2.0.1", "xbad%ver", false ]
547
- ]
548
-
549
- operatorTests.each do |params|
550
- op = params[0]
551
- value1 = params[1]
552
- value2 = params[2]
553
- shouldBe = params[3]
554
- it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
555
- user = { key: 'x', custom: { foo: value1 } }
556
- clause = { attribute: 'foo', op: op, values: [value2] }
557
- flag = boolean_flag_with_clauses([clause])
558
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be shouldBe
559
- end
560
- end
561
- end
562
-
563
- describe "variation_index_for_user" do
564
- it "matches bucket" do
565
- user = { key: "userkey" }
566
- flag_key = "flagkey"
567
- salt = "salt"
568
-
569
- # First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
570
- # so we can construct a rollout whose second bucket just barely contains that value
571
- bucket_value = (bucket_user(user, flag_key, "key", salt) * 100000).truncate()
572
- expect(bucket_value).to be > 0
573
- expect(bucket_value).to be < 100000
574
-
575
- bad_variation_a = 0
576
- matched_variation = 1
577
- bad_variation_b = 2
578
- rule = {
579
- rollout: {
580
- variations: [
581
- { variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
582
- { variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
583
- { variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
584
- ]
585
- }
586
- }
587
- flag = { key: flag_key, salt: salt }
588
-
589
- result_variation = variation_index_for_user(flag, rule, user)
590
- expect(result_variation).to be matched_variation
591
- end
592
-
593
- it "uses last bucket if bucket value is equal to total weight" do
594
- user = { key: "userkey" }
595
- flag_key = "flagkey"
596
- salt = "salt"
597
-
598
- bucket_value = (bucket_user(user, flag_key, "key", salt) * 100000).truncate()
599
-
600
- # We'll construct a list of variations that stops right at the target bucket value
601
- rule = {
602
- rollout: {
603
- variations: [
604
- { variation: 0, weight: bucket_value }
605
- ]
606
- }
607
- }
608
- flag = { key: flag_key, salt: salt }
609
-
610
- result_variation = variation_index_for_user(flag, rule, user)
611
- expect(result_variation).to be 0
612
- end
613
- end
614
-
615
- describe "bucket_user" do
616
- it "gets expected bucket values for specific keys" do
617
- user = { key: "userKeyA" }
618
- bucket = bucket_user(user, "hashKey", "key", "saltyA")
619
- expect(bucket).to be_within(0.0000001).of(0.42157587);
620
-
621
- user = { key: "userKeyB" }
622
- bucket = bucket_user(user, "hashKey", "key", "saltyA")
623
- expect(bucket).to be_within(0.0000001).of(0.6708485);
624
-
625
- user = { key: "userKeyC" }
626
- bucket = bucket_user(user, "hashKey", "key", "saltyA")
627
- expect(bucket).to be_within(0.0000001).of(0.10343106);
628
- end
629
-
630
- it "can bucket by int value (equivalent to string)" do
631
- user = {
632
- key: "userkey",
633
- custom: {
634
- stringAttr: "33333",
635
- intAttr: 33333
636
- }
637
- }
638
- stringResult = bucket_user(user, "hashKey", "stringAttr", "saltyA")
639
- intResult = bucket_user(user, "hashKey", "intAttr", "saltyA")
640
-
641
- expect(intResult).to be_within(0.0000001).of(0.54771423)
642
- expect(intResult).to eq(stringResult)
643
- end
644
-
645
- it "cannot bucket by float value" do
646
- user = {
647
- key: "userkey",
648
- custom: {
649
- floatAttr: 33.5
650
- }
651
- }
652
- result = bucket_user(user, "hashKey", "floatAttr", "saltyA")
653
- expect(result).to eq(0.0)
654
- end
655
-
656
-
657
- it "cannot bucket by bool value" do
658
- user = {
659
- key: "userkey",
660
- custom: {
661
- boolAttr: true
662
- }
663
- }
664
- result = bucket_user(user, "hashKey", "boolAttr", "saltyA")
665
- expect(result).to eq(0.0)
666
- end
667
- end
668
-
669
- def make_segment(key)
670
- {
671
- key: key,
672
- included: [],
673
- excluded: [],
674
- salt: 'abcdef',
675
- version: 1
676
- }
677
- end
678
-
679
- def make_segment_match_clause(segment)
680
- {
681
- op: :segmentMatch,
682
- values: [ segment[:key] ],
683
- negate: false
684
- }
685
- end
686
-
687
- def make_user_matching_clause(user, attr)
688
- {
689
- attribute: attr.to_s,
690
- op: :in,
691
- values: [ user[attr.to_sym] ],
692
- negate: false
693
- }
694
- end
695
-
696
- describe 'segment matching' do
697
- def test_segment_match(segment)
698
- features.upsert(LaunchDarkly::SEGMENTS, segment)
699
- clause = make_segment_match_clause(segment)
700
- flag = boolean_flag_with_clauses([clause])
701
- evaluate(flag, user, features, logger, factory).detail.value
702
- end
703
-
704
- it 'explicitly includes user' do
705
- segment = make_segment('segkey')
706
- segment[:included] = [ user[:key] ]
707
- expect(test_segment_match(segment)).to be true
708
- end
709
-
710
- it 'explicitly excludes user' do
711
- segment = make_segment('segkey')
712
- segment[:excluded] = [ user[:key] ]
713
- expect(test_segment_match(segment)).to be false
714
- end
715
-
716
- it 'both includes and excludes user; include takes priority' do
717
- segment = make_segment('segkey')
718
- segment[:included] = [ user[:key] ]
719
- segment[:excluded] = [ user[:key] ]
720
- expect(test_segment_match(segment)).to be true
721
- end
722
-
723
- it 'matches user by rule when weight is absent' do
724
- segClause = make_user_matching_clause(user, :email)
725
- segRule = {
726
- clauses: [ segClause ]
727
- }
728
- segment = make_segment('segkey')
729
- segment[:rules] = [ segRule ]
730
- expect(test_segment_match(segment)).to be true
731
- end
732
-
733
- it 'matches user by rule when weight is nil' do
734
- segClause = make_user_matching_clause(user, :email)
735
- segRule = {
736
- clauses: [ segClause ],
737
- weight: nil
738
- }
739
- segment = make_segment('segkey')
740
- segment[:rules] = [ segRule ]
741
- expect(test_segment_match(segment)).to be true
742
- end
743
-
744
- it 'matches user with full rollout' do
745
- segClause = make_user_matching_clause(user, :email)
746
- segRule = {
747
- clauses: [ segClause ],
748
- weight: 100000
749
- }
750
- segment = make_segment('segkey')
751
- segment[:rules] = [ segRule ]
752
- expect(test_segment_match(segment)).to be true
753
- end
754
-
755
- it "doesn't match user with zero rollout" do
756
- segClause = make_user_matching_clause(user, :email)
757
- segRule = {
758
- clauses: [ segClause ],
759
- weight: 0
760
- }
761
- segment = make_segment('segkey')
762
- segment[:rules] = [ segRule ]
763
- expect(test_segment_match(segment)).to be false
764
- end
765
-
766
- it "matches user with multiple clauses" do
767
- segClause1 = make_user_matching_clause(user, :email)
768
- segClause2 = make_user_matching_clause(user, :name)
769
- segRule = {
770
- clauses: [ segClause1, segClause2 ]
771
- }
772
- segment = make_segment('segkey')
773
- segment[:rules] = [ segRule ]
774
- expect(test_segment_match(segment)).to be true
775
- end
776
-
777
- it "doesn't match user with multiple clauses if a clause doesn't match" do
778
- segClause1 = make_user_matching_clause(user, :email)
779
- segClause2 = make_user_matching_clause(user, :name)
780
- segClause2[:values] = [ 'wrong' ]
781
- segRule = {
782
- clauses: [ segClause1, segClause2 ]
783
- }
784
- segment = make_segment('segkey')
785
- segment[:rules] = [ segRule ]
786
- expect(test_segment_match(segment)).to be false
787
- end
788
- end
789
- end