1 package ldcontext
2
3 import (
4 "fmt"
5 "testing"
6
7 "github.com/launchdarkly/go-sdk-common/v3/ldattr"
8 "github.com/launchdarkly/go-sdk-common/v3/lderrors"
9 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
10
11 "github.com/launchdarkly/go-test-helpers/v3/jsonhelpers"
12
13 "github.com/stretchr/testify/assert"
14 )
15
16 type invalidKindTestParams struct {
17 kind string
18 err error
19 }
20
21 func makeInvalidKindTestParams() []invalidKindTestParams {
22 return []invalidKindTestParams{
23 {"kind", lderrors.ErrContextKindCannotBeKind{}},
24 {"multi", lderrors.ErrContextKindMultiForSingleKind{}},
25 {"örg", lderrors.ErrContextKindInvalidChars{}},
26 {"o~rg", lderrors.ErrContextKindInvalidChars{}},
27 {"😀rg", lderrors.ErrContextKindInvalidChars{}},
28 {"o\trg", lderrors.ErrContextKindInvalidChars{}},
29 }
30 }
31
32 func makeBasicBuilder() *Builder {
33
34 return NewBuilder("my-key")
35 }
36
37 func TestBuilderDefaultProperties(t *testing.T) {
38 c := NewBuilder("my-key").Build()
39 assert.True(t, c.IsDefined())
40 assert.NoError(t, c.Err())
41 assert.Equal(t, DefaultKind, c.Kind())
42 assert.Equal(t, "my-key", c.Key())
43
44 assert.Equal(t, ldvalue.OptionalString{}, c.Name())
45 assert.False(t, c.Anonymous())
46 assert.Equal(t, ldvalue.OptionalString{}, c.Secondary())
47 assert.Len(t, c.GetOptionalAttributeNames(nil), 0)
48 }
49
50 func TestBuilderKindValidation(t *testing.T) {
51 for _, p := range makeInvalidKindTestParams() {
52 t.Run(p.kind, func(t *testing.T) {
53 b := NewBuilder("my-key").Kind(Kind(p.kind))
54
55 c0 := b.Build()
56 assert.True(t, c0.IsDefined())
57 assert.Equal(t, p.err, c0.Err())
58
59 c1, err := b.TryBuild()
60 assert.True(t, c1.IsDefined())
61 assert.Equal(t, p.err, c1.Err())
62 assert.Equal(t, p.err, err)
63 })
64 }
65 }
66
67 func TestBuilderKeyValidation(t *testing.T) {
68 b := NewBuilder("")
69
70 c0 := b.Build()
71 assert.True(t, c0.IsDefined())
72 assert.Equal(t, lderrors.ErrContextKeyEmpty{}, c0.Err())
73
74 c1, err := b.TryBuild()
75 assert.True(t, c1.IsDefined())
76 assert.Equal(t, lderrors.ErrContextKeyEmpty{}, c1.Err())
77 assert.Equal(t, lderrors.ErrContextKeyEmpty{}, err)
78 }
79
80 func TestBuilderFullyQualifiedKey(t *testing.T) {
81 t.Run("kind is user", func(t *testing.T) {
82 c := New("my-user-key")
83 assert.Equal(t, "my-user-key", c.FullyQualifiedKey())
84 })
85
86 t.Run("kind is not user", func(t *testing.T) {
87 c := NewWithKind("org", "my-org-key")
88 assert.Equal(t, "org:my-org-key", c.FullyQualifiedKey())
89 })
90
91 t.Run("key is escaped", func(t *testing.T) {
92 c := NewWithKind("org", "my:key%x/y")
93 assert.Equal(t, "org:my%3Akey%25x/y", c.FullyQualifiedKey())
94 })
95 }
96
97 func TestBuilderBasicSetters(t *testing.T) {
98 t.Run("Kind", func(t *testing.T) {
99 assert.Equal(t, Kind("org"), NewBuilder("my-key").Kind("org").Build().Kind())
100
101 assert.Equal(t, DefaultKind, NewBuilder("my-key").Kind("").Build().Kind())
102 })
103
104 t.Run("Key", func(t *testing.T) {
105 assert.Equal(t, "other-key", NewBuilder("my-key").Key("other-key").Build().Key())
106 })
107
108 t.Run("Name", func(t *testing.T) {
109 c0 := makeBasicBuilder().Build()
110 assert.Equal(t, ldvalue.OptionalString{}, c0.Name())
111
112 c1 := makeBasicBuilder().Name("my-name").Build()
113 assert.Equal(t, ldvalue.NewOptionalString("my-name"), c1.Name())
114
115 c2 := makeBasicBuilder().OptName(ldvalue.OptionalString{}).Build()
116 assert.Equal(t, ldvalue.OptionalString{}, c2.Name())
117
118 c3 := makeBasicBuilder().OptName(ldvalue.NewOptionalString("my-name")).Build()
119 assert.Equal(t, ldvalue.NewOptionalString("my-name"), c3.Name())
120 })
121
122 t.Run("Secondary", func(t *testing.T) {
123 c0 := makeBasicBuilder().Build()
124 assert.Equal(t, ldvalue.OptionalString{}, c0.Secondary())
125 })
126
127 t.Run("Anonymous", func(t *testing.T) {
128 c0 := makeBasicBuilder().Build()
129 assert.False(t, c0.Anonymous())
130
131 c1 := makeBasicBuilder().Anonymous(false).Build()
132 assert.False(t, c1.Anonymous())
133
134 c2 := makeBasicBuilder().Anonymous(true).Build()
135 assert.True(t, c2.Anonymous())
136 })
137 }
138
139 func TestBuilderSetCustomAttributes(t *testing.T) {
140 t.Run("SetValue", func(t *testing.T) {
141 otherValue := ldvalue.String("other-value")
142 for _, value := range []ldvalue.Value{
143 ldvalue.Bool(true),
144 ldvalue.Bool(false),
145 ldvalue.Int(0),
146 ldvalue.Int(1),
147 ldvalue.String(""),
148 ldvalue.String("x"),
149 ldvalue.ArrayOf(ldvalue.Int(1), ldvalue.Int(2)),
150 ldvalue.ObjectBuild().Set("a", ldvalue.Int(1)).Build(),
151 } {
152 t.Run(value.JSONString(), func(t *testing.T) {
153 c := makeBasicBuilder().
154 SetValue("my-attr", value).
155 SetValue("other-attr", otherValue).
156 Build()
157 assert.Len(t, c.attributes.Keys(nil), 2)
158 jsonhelpers.AssertEqual(t, value, c.attributes.Get("my-attr"))
159 jsonhelpers.AssertEqual(t, otherValue, c.attributes.Get("other-attr"))
160 })
161 }
162 })
163
164 t.Run("typed setters", func(t *testing.T) {
165
166 assert.Equal(t,
167 makeBasicBuilder().SetValue("my-attr", ldvalue.Bool(true)),
168 makeBasicBuilder().SetBool("my-attr", true))
169 assert.Equal(t,
170 makeBasicBuilder().SetValue("my-attr", ldvalue.Int(100)),
171 makeBasicBuilder().SetInt("my-attr", 100))
172 assert.Equal(t,
173 makeBasicBuilder().SetValue("my-attr", ldvalue.Float64(1.5)),
174 makeBasicBuilder().SetFloat64("my-attr", 1.5))
175 assert.Equal(t,
176 makeBasicBuilder().SetValue("my-attr", ldvalue.String("x")),
177 makeBasicBuilder().SetString("my-attr", "x"))
178 })
179
180 t.Run("setting to null does not add attribute", func(t *testing.T) {
181 assert.Equal(t,
182 makeBasicBuilder().SetString("attr1", "value1").SetString("attr3", "value3"),
183 makeBasicBuilder().SetString("attr1", "value1").SetValue("attr2", ldvalue.Null()).SetString("attr3", "value3"))
184 })
185
186 t.Run("setting to null removes existing attribute", func(t *testing.T) {
187 assert.Equal(t,
188 makeBasicBuilder().SetString("attr1", "value1").SetString("attr3", "value3"),
189 makeBasicBuilder().SetString("attr1", "value1").SetString("attr2", "value2").SetString("attr3", "value3").
190 SetValue("attr2", ldvalue.Null()))
191 })
192
193 t.Run("cannot add attribute with empty name", func(t *testing.T) {
194 assert.Equal(t, makeBasicBuilder().Build(), makeBasicBuilder().SetBool("", true).Build())
195 assert.Equal(t, makeBasicBuilder().Build(), makeBasicBuilder().SetInt("", 1).Build())
196 assert.Equal(t, makeBasicBuilder().Build(), makeBasicBuilder().SetFloat64("", 1).Build())
197 assert.Equal(t, makeBasicBuilder().Build(), makeBasicBuilder().SetString("", "x").Build())
198 assert.Equal(t, makeBasicBuilder().Build(), makeBasicBuilder().SetValue("", ldvalue.ArrayOf()).Build())
199 })
200 }
201
202 func TestBuilderSetBuiltInAttributesByName(t *testing.T) {
203 var boolFalse, boolTrue, stringEmpty, stringNonEmpty = ldvalue.Bool(false), ldvalue.Bool(true),
204 ldvalue.String("x"), ldvalue.String("")
205 var nullValue, intValue, floatValue, arrayValue, objectValue = ldvalue.Null(),
206 ldvalue.Int(1), ldvalue.Float64(1.5), ldvalue.ArrayOf(), ldvalue.ObjectBuild().Build()
207
208 type params struct {
209 name string
210 equivalentSetter func(*Builder, ldvalue.Value)
211 good, bad []ldvalue.Value
212 }
213
214 for _, p := range []params{
215 {
216 name: "kind",
217 equivalentSetter: func(b *Builder, v ldvalue.Value) { b.Kind(Kind(v.StringValue())) },
218 good: []ldvalue.Value{stringNonEmpty, stringEmpty},
219 bad: []ldvalue.Value{nullValue, boolFalse, intValue, floatValue, arrayValue, objectValue},
220 },
221 {
222 name: "key",
223 equivalentSetter: func(b *Builder, v ldvalue.Value) { b.Key(v.StringValue()) },
224 good: []ldvalue.Value{stringNonEmpty, stringEmpty},
225 bad: []ldvalue.Value{nullValue, boolFalse, intValue, floatValue, arrayValue, objectValue},
226 },
227 {
228 name: "name",
229 equivalentSetter: func(b *Builder, v ldvalue.Value) { b.OptName(v.AsOptionalString()) },
230 good: []ldvalue.Value{stringNonEmpty, stringEmpty, nullValue},
231 bad: []ldvalue.Value{boolFalse, intValue, floatValue, arrayValue, objectValue},
232 },
233 {
234 name: "anonymous",
235 equivalentSetter: func(b *Builder, v ldvalue.Value) { b.Anonymous(v.BoolValue()) },
236 good: []ldvalue.Value{boolTrue, boolFalse},
237 bad: []ldvalue.Value{nullValue, intValue, floatValue, stringEmpty, stringNonEmpty, arrayValue, objectValue},
238 },
239 } {
240 t.Run(p.name, func(t *testing.T) {
241 builder := makeBasicBuilder()
242 var lastGoodNonNullValue ldvalue.Value
243
244 for _, goodValue := range p.good {
245 t.Run(fmt.Sprintf("can set to %s", goodValue.JSONString()), func(t *testing.T) {
246 previousState := *builder
247
248 if !goodValue.IsNull() {
249 lastGoodNonNullValue = goodValue
250 }
251 expected := makeBasicBuilder()
252 p.equivalentSetter(expected, goodValue)
253
254 builder.SetValue(p.name, goodValue)
255 assert.Equal(t, expected, builder)
256
257 b1 := previousState
258 assert.True(t, b1.TrySetValue(p.name, goodValue))
259 assert.Equal(t, *expected, b1)
260
261 b2 := previousState
262 switch goodValue.Type() {
263 case ldvalue.BoolType:
264 assert.Equal(t, expected, b2.SetBool(p.name, goodValue.BoolValue()))
265 case ldvalue.StringType:
266 assert.Equal(t, expected, b2.SetString(p.name, goodValue.StringValue()))
267 }
268 })
269 }
270 for _, badValue := range p.bad {
271 t.Run(fmt.Sprintf("cannot set to %s", badValue.JSONString()), func(t *testing.T) {
272 startingState := func() *Builder {
273 if lastGoodNonNullValue.IsDefined() {
274 return makeBasicBuilder().SetValue(p.name, lastGoodNonNullValue)
275 }
276 return makeBasicBuilder()
277 }
278
279 assert.Equal(t, startingState(), startingState().SetValue(p.name, badValue))
280
281 b := startingState()
282 assert.False(t, b.TrySetValue(p.name, badValue))
283 assert.Equal(t, startingState(), b)
284
285 switch badValue.Type() {
286 case ldvalue.BoolType:
287 assert.Equal(t, startingState(), startingState().SetBool(p.name, badValue.BoolValue()))
288 case ldvalue.NumberType:
289 if badValue.IsInt() {
290 assert.Equal(t, startingState(), startingState().SetInt(p.name, badValue.IntValue()))
291 } else {
292 assert.Equal(t, startingState(), startingState().SetFloat64(p.name, badValue.Float64Value()))
293 }
294 case ldvalue.StringType:
295 assert.Equal(t, startingState(), makeBasicBuilder().SetString(p.name, badValue.StringValue()))
296 }
297 })
298 }
299 })
300 }
301 }
302
303 func TestBuilderSetValueCannotSetMetaProperties(t *testing.T) {
304 for _, p := range []struct {
305 name string
306 value ldvalue.Value
307 }{
308 {"secondary", ldvalue.String("x")},
309 {"privateAttributes", ldvalue.ArrayOf(ldvalue.String("x"))},
310 } {
311 t.Run(p.name, func(t *testing.T) {
312 c := makeBasicBuilder().SetValue(p.name, p.value).Build()
313 assert.Equal(t, p.value, c.attributes.Get(p.name))
314 assert.Equal(t, ldvalue.OptionalString{}, c.secondary)
315 assert.Len(t, c.privateAttrs, 0)
316 })
317 }
318
319 t.Run("_meta", func(t *testing.T) {
320 b := makeBasicBuilder()
321 assert.False(t, b.TrySetValue("_meta", ldvalue.String("hi")))
322 assert.Equal(t, 0, b.Build().attributes.Count())
323 })
324 }
325
326 func TestBuilderAttributesCopyOnWrite(t *testing.T) {
327 value1, value2 := ldvalue.String("value1"), ldvalue.String("value2")
328
329 b := makeBasicBuilder().SetValue("attr", value1)
330
331 c1 := b.Build()
332 jsonhelpers.AssertEqual(t, value1, c1.attributes.Get("attr"))
333
334 b.SetValue("attr", value2)
335
336 c2 := b.Build()
337 jsonhelpers.AssertEqual(t, value2, c2.attributes.Get("attr"))
338 jsonhelpers.AssertEqual(t, value1, c1.attributes.Get("attr"))
339 }
340
341 func TestBuilderPrivate(t *testing.T) {
342 expectPrivateRefsToBe := func(t *testing.T, c Context, expectedRefs ...ldattr.Ref) {
343 if assert.Equal(t, len(expectedRefs), c.PrivateAttributeCount()) {
344 for i, expectedRef := range expectedRefs {
345 a, ok := c.PrivateAttributeByIndex(i)
346 assert.True(t, ok)
347 assert.Equal(t, expectedRef, a)
348 }
349 _, ok := c.PrivateAttributeByIndex(len(expectedRefs))
350 assert.False(t, ok)
351 }
352 _, ok := c.PrivateAttributeByIndex(-1)
353 assert.False(t, ok)
354 }
355
356 t.Run("using Refs", func(t *testing.T) {
357 attrRef1, attrRef2, attrRef3 := ldattr.NewRef("a"), ldattr.NewRef("/b/c"), ldattr.NewRef("d")
358 c := makeBasicBuilder().
359 PrivateRef(attrRef1, attrRef2).PrivateRef(attrRef3).
360 Build()
361
362 expectPrivateRefsToBe(t, c, attrRef1, attrRef2, attrRef3)
363 })
364
365 t.Run("using strings", func(t *testing.T) {
366 s1, s2, s3 := "a", "/b/c", "d"
367 b0 := makeBasicBuilder().
368 PrivateRef(ldattr.NewRef(s1), ldattr.NewRef(s2)).PrivateRef(ldattr.NewRef(s3))
369 b1 := makeBasicBuilder().
370 Private(s1, s2, s3)
371 assert.Equal(t, b0, b1)
372 })
373
374 t.Run("RemovePrivate", func(t *testing.T) {
375 b := makeBasicBuilder().Private("a", "/b/c", "d", "/b/c")
376 b.RemovePrivate("/b/c")
377 c := b.Build()
378
379 expectPrivateRefsToBe(t, c, ldattr.NewRef("a"), ldattr.NewRef("d"))
380 })
381
382 t.Run("RemovePrivateRef", func(t *testing.T) {
383 b := makeBasicBuilder().Private("a", "/b/c", "d", "/b/c")
384 b.RemovePrivateRef(ldattr.NewRef("/b/c"))
385 c := b.Build()
386
387 expectPrivateRefsToBe(t, c, ldattr.NewRef("a"), ldattr.NewRef("d"))
388 })
389
390 t.Run("copy on write", func(t *testing.T) {
391 b0 := makeBasicBuilder().Private("a")
392
393 c0 := b0.Build()
394 expectPrivateRefsToBe(t, c0, ldattr.NewRef("a"))
395
396 b0.Private("b")
397 c1 := b0.Build()
398 expectPrivateRefsToBe(t, c1, ldattr.NewRef("a"), ldattr.NewRef("b"))
399 expectPrivateRefsToBe(t, c0, ldattr.NewRef("a"))
400
401 b0.RemovePrivateRef(ldattr.NewRef("a"))
402 c2 := b0.Build()
403 expectPrivateRefsToBe(t, c2, ldattr.NewRef("b"))
404 expectPrivateRefsToBe(t, c1, ldattr.NewRef("a"), ldattr.NewRef("b"))
405 expectPrivateRefsToBe(t, c0, ldattr.NewRef("a"))
406 })
407 }
408
409 func TestNewBuilderFromContext(t *testing.T) {
410 value1, value2 := ldvalue.String("value1"), ldvalue.String("value2")
411
412 b1 := NewBuilder("key1").Kind("kind1").Name("name1").Anonymous(true).SetValue("attr", value1)
413 b1.Private("private1")
414 c1 := b1.Build()
415 jsonhelpers.AssertEqual(t, value1, c1.attributes.Get("attr"))
416 assert.Len(t, c1.privateAttrs, 1)
417
418 b2 := NewBuilderFromContext(c1)
419 c2 := b2.Build()
420 assert.Equal(t, Kind("kind1"), c2.Kind())
421 assert.Equal(t, "key1", c2.Key())
422 assert.True(t, c2.Anonymous())
423 jsonhelpers.AssertEqual(t, value1, c2.attributes.Get("attr"))
424 assert.Equal(t, c1.privateAttrs, c2.privateAttrs)
425
426 b3 := NewBuilderFromContext(c1)
427 b3.SetValue("attr", value2)
428 b3.Private("private2")
429 c3 := b3.Build()
430 jsonhelpers.AssertEqual(t, value2, c3.attributes.Get("attr"))
431 jsonhelpers.AssertEqual(t, value1, c1.attributes.Get("attr"))
432 assert.Len(t, c3.privateAttrs, 2)
433 assert.Len(t, c1.privateAttrs, 1)
434
435 multi := NewMulti(NewWithKind("kind1", "key1"), NewWithKind("kind2", "key2"))
436 assert.NoError(t, multi.Err())
437 c4 := NewBuilderFromContext(multi).Build()
438 assert.Error(t, c4.Err())
439 }
440
441 func TestBuilderSafety(t *testing.T) {
442
443 var emptyInstance Builder
444 emptyInstance.Key("a")
445 assert.Equal(t, New("a"), emptyInstance.Build())
446
447
448 var nilPtr *Builder
449 assert.Nil(t, nilPtr.Key("a"))
450 assert.Nil(t, nilPtr.Name("a"))
451 assert.Nil(t, nilPtr.Anonymous(true))
452 assert.Nil(t, nilPtr.SetValue("a", ldvalue.Bool(true)))
453 assert.Nil(t, nilPtr.Private("a"))
454 assert.Nil(t, nilPtr.RemovePrivate("a"))
455 assert.Equal(t, Context{}, nilPtr.Build())
456 }
457
View as plain text