Skip to content
Merged
16 changes: 8 additions & 8 deletions sdk-common/__tests__/Context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,21 @@ describe.each([
});

it('should get the same values', () => {
expect(context?.valueForKind('user', new AttributeReference('cat'))).toEqual('calico');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the order of these params to allow for the kind to be optional and simplify evaluation logic.

expect(context?.valueForKind('user', new AttributeReference('name'))).toEqual('context name');
expect(context?.valueForKind(new AttributeReference('cat'), 'user')).toEqual('calico');
expect(context?.valueForKind(new AttributeReference('name'), 'user')).toEqual('context name');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal, but: I'm unclear on why you're using context? in all the tests like this. I mean, if line 63 fails somehow and context ends up being null/undefined, then the "should create a context" test will fail with a clear error. And I would think that if these other tests failed by throwing a null reference exception, that would also be fairly clear. Using ?. in all these expectations means, if I understand correctly, that it will instead be telling us "expected for valueForKind to return 'calico', but it returned undefined", etc.— which is a bit misleading since the problem has nothing to do with valueForKind. Plus, if we got past the first line, we know context is not null/undefined, so what's the point of repeating ?. every time?

expect(context?.kinds).toStrictEqual(['user']);
expect(context?.kindsAndKeys).toStrictEqual({ user: 'test' });
// Canonical keys for 'user' contexts are just the key.
expect(context?.canonicalKey).toEqual('test');
expect(context?.valueForKind('user', new AttributeReference('transient'))).toBeTruthy();
expect(context?.valueForKind(new AttributeReference('transient'), 'user')).toBeTruthy();
expect(context?.secondary('user')).toEqual('secondary');
expect(context?.isMultiKind).toBeFalsy();
expect(context?.privateAttributes('user')?.[0].redactionName)
.toEqual(new AttributeReference('/~1dog~0~0~1~1').redactionName);
});

it('should not get values for a context kind that does not exist', () => {
expect(context?.valueForKind('org', new AttributeReference('cat'))).toBeUndefined();
expect(context?.valueForKind(new AttributeReference('cat'), 'org')).toBeUndefined();
});

it('should have the correct kinds', () => {
Expand All @@ -108,12 +108,12 @@ describe('given a valid legacy user without custom attributes', () => {
});

it('should get expected values', () => {
expect(context?.valueForKind('user', new AttributeReference('name'))).toEqual('context name');
expect(context?.valueForKind(new AttributeReference('name'), 'user')).toEqual('context name');
expect(context?.kinds).toStrictEqual(['user']);
expect(context?.kindsAndKeys).toStrictEqual({ user: 'test' });
// Canonical keys for 'user' contexts are just the key.
expect(context?.canonicalKey).toEqual('test');
expect(context?.valueForKind('user', new AttributeReference('transient'))).toBeTruthy();
expect(context?.valueForKind(new AttributeReference('transient'), 'user')).toBeTruthy();
expect(context?.secondary('user')).toEqual('secondary');
expect(context?.isMultiKind).toBeFalsy();
expect(context?.privateAttributes('user')?.[0].redactionName)
Expand Down Expand Up @@ -204,8 +204,8 @@ describe('given a multi-kind context', () => {
});

it('should get values from the correct context', () => {
expect(context?.valueForKind('org', new AttributeReference('value'))).toEqual('OrgValue');
expect(context?.valueForKind('user', new AttributeReference('value'))).toEqual('UserValue');
expect(context?.valueForKind(new AttributeReference('value'), 'org')).toEqual('OrgValue');
expect(context?.valueForKind(new AttributeReference('value'), 'user')).toEqual('UserValue');

expect(context?.secondary('org')).toEqual('value');
expect(context?.secondary('user')).toBeUndefined();
Expand Down
25 changes: 21 additions & 4 deletions sdk-common/src/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { TypeValidators } from './validators';
// Validates a kind excluding check that it isn't "kind".
const KindValidator = TypeValidators.StringMatchingRegex(/^(\w|\.|-)+$/);

// When no kind is specified, then this kind will be used.
const DEFAULT_KIND = 'user';

// The API allows for calling with an `LDContext` which is
// `LDUser | LDSingleKindContext | LDMultiKindContext`. When ingesting a context
// first the type must be determined to allow us to put it into a consistent type.
Expand Down Expand Up @@ -170,6 +173,8 @@ export default class Context {

public readonly kind: string;

static readonly userKind: string = DEFAULT_KIND;

/**
* Contexts should be created using the static factory method {@link Context.FromLDContext}.
* @param kind The kind of the context.
Expand Down Expand Up @@ -306,23 +311,35 @@ export default class Context {

/**
* Attempt to get a value for the given context kind using the given reference.
* @param kind The kind of the context to get the value for.
* @param reference The reference to the value to get.
* @param kind The kind of the context to get the value for.
* @returns a value or `undefined` if one is not found.
*/
public valueForKind(kind: string, reference: AttributeReference): any | undefined {
public valueForKind(
reference: AttributeReference,
kind: string = DEFAULT_KIND,
): any | undefined {
return Context.getValueFromContext(reference, this.contextForKind(kind));
}

/**
* Attempt to get a key for the specified kind.
* @param kind The kind to get a key for.
* @returns The key for the specified kind, or undefined.
*/
public key(kind: string = DEFAULT_KIND): string | undefined {
return this.contextForKind(kind)?.key;
}

/**
* Attempt to get a secondary key from a context.
* @param kind The kind of the context to get the secondary key for.
* @returns the secondary key, or undefined if not present or not a string.
*/
public secondary(kind: string): string | undefined {
public secondary(kind: string = DEFAULT_KIND): string | undefined {
const context = this.contextForKind(kind);
if (defined(context?._meta?.secondary)
&& TypeValidators.String.is(context?._meta?.secondary)) {
&& TypeValidators.String.is(context?._meta?.secondary)) {
return context?._meta?.secondary;
}
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion server-sdk-common/__tests__/evaluation/Bucketer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe.each<[
// The hasher always returns the same value. This just checks that it converts it to a number
// in the expected way.
expect(
bucketer.bucket(validatedContext!, key, attrRef, salt, isExperiment, kindForRollout, seed)
bucketer.bucket(validatedContext!, key, attrRef, salt, isExperiment, kindForRollout, seed),
).toBeCloseTo(0.07111111110140983, 5);
expect(hasher.update).toHaveBeenCalledWith(expected);
expect(hasher.digest).toHaveBeenCalledWith('hex');
Expand Down
127 changes: 127 additions & 0 deletions server-sdk-common/__tests__/evaluation/Evaluator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Context, LDContext } from '@launchdarkly/js-sdk-common';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there are very limited tests for just the couple things the evaluator supports.

import { BigSegmentStoreMembership } from '../../src/api/interfaces';
import { Flag } from '../../src/evaluation/data/Flag';
import { Segment } from '../../src/evaluation/data/Segment';
import EvalResult from '../../src/evaluation/EvalResult';
import Evaluator from '../../src/evaluation/Evaluator';
import { Queries } from '../../src/evaluation/Queries';
import Reasons from '../../src/evaluation/Reasons';

const offBaseFlag = {
key: 'feature0',
version: 1,
on: false,
fallthrough: { variation: 1 },
variations: [
'zero',
'one',
'two',
],
};

const noQueries: Queries = {
getFlag(): Promise<Flag | null> {
throw new Error('Function not implemented.');
},
getSegment(): Promise<Segment | null> {
throw new Error('Function not implemented.');
},
getBigSegmentsMembership(): Promise<BigSegmentStoreMembership | null> {
throw new Error('Function not implemented.');
},
};

describe.each<[Flag, LDContext, EvalResult | undefined]>([
[{
...offBaseFlag,
}, { key: 'user-key' }, EvalResult.ForSuccess(null, Reasons.Off, undefined)],
[{
...offBaseFlag, offVariation: 2,
}, { key: 'user-key' }, EvalResult.ForSuccess('two', Reasons.Off, 2)],
])('Given off flags and an evaluator', (flag, context, expected) => {
const evaluator = new Evaluator(noQueries);

// @ts-ignore
it(`produces the expected evaluation result for context: ${context.key} ${context.kind} targets: ${flag.targets?.map((t) => `${t.values}, ${t.variation}`)} context targets: ${flag.contextTargets?.map((t) => `${t.contextKind}: ${t.values}, ${t.variation}`)}`, async () => {
const result = await evaluator.evaluate(flag, Context.FromLDContext(context)!);
expect(result?.isError).toEqual(expected?.isError);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my earlier comment, I think I would prefer a more specific failure here if result is null/undefined (which it shouldn't ever be, right?).

expect(result?.detail).toStrictEqual(expected?.detail);
expect(result?.message).toEqual(expected?.message);
});
});

const targetBaseFlag = {
key: 'feature0',
version: 1,
on: true,
fallthrough: { variation: 1 },
variations: [
'zero',
'one',
'two',
],
};

describe.each<[Flag, LDContext, EvalResult | undefined]>([
[{
...targetBaseFlag,
targets: [{
values: ['user-key'],
variation: 0,
}],
}, { key: 'user-key' }, EvalResult.ForSuccess('zero', Reasons.TargetMatch, 0)],
[{
...targetBaseFlag,
targets: [{
values: ['user-key'],
variation: 0,
},
{
values: ['user-key2'],
variation: 2,
},
],
}, { key: 'user-key2' }, EvalResult.ForSuccess('two', Reasons.TargetMatch, 2)],
[{
...targetBaseFlag,
targets: [{
values: ['user-key'],
variation: 0,
},
{
values: ['user-key2'],
variation: 2,
},
],
contextTargets: [{
values: [],
variation: 2,
}],
}, { key: 'user-key2' }, EvalResult.ForSuccess('two', Reasons.TargetMatch, 2)],
[{
...targetBaseFlag,
targets: [{
values: ['user-key'],
variation: 0,
},
{
values: ['user-key2'],
variation: 2,
},
],
contextTargets: [{
contextKind: 'org',
values: ['org-key'],
variation: 1,
}],
}, { kind: 'org', key: 'org-key' }, EvalResult.ForSuccess('one', Reasons.TargetMatch, 1)],
])('given flag configurations with different targets that match', (flag, context, expected) => {
const evaluator = new Evaluator(noQueries);
// @ts-ignore
it(`produces the expected evaluation result for context: ${context.key} ${context.kind} targets: ${flag.targets?.map((t) => `${t.values}, ${t.variation}`)} context targets: ${flag.contextTargets?.map((t) => `${t.contextKind}: ${t.values}, ${t.variation}`)}`, async () => {
const result = await evaluator.evaluate(flag, Context.FromLDContext(context)!);
expect(result?.isError).toEqual(expected?.isError);
expect(result?.detail).toStrictEqual(expected?.detail);
expect(result?.message).toEqual(expected?.message);
});
});
120 changes: 120 additions & 0 deletions server-sdk-common/__tests__/evaluation/evalTargets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Context, LDContext } from '@launchdarkly/js-sdk-common';
import { Flag } from '../../src/evaluation/data/Flag';
import EvalResult from '../../src/evaluation/EvalResult';
import evalTargets from '../../src/evaluation/evalTargets';
import Reasons from '../../src/evaluation/Reasons';

const baseFlag = {
key: 'feature0',
version: 1,
on: true,
fallthrough: { variation: 1 },
variations: [
'zero',
'one',
'two',
],
};

describe.each<[Flag, LDContext, EvalResult | undefined]>([
[{
...baseFlag,
targets: [{
values: ['user-key'],
variation: 0,
}],
}, { key: 'user-key' }, EvalResult.ForSuccess('zero', Reasons.TargetMatch, 0)],
[{
...baseFlag,
targets: [{
values: ['user-key'],
variation: 0,
}],
}, { key: 'different-key' }, undefined],
[{
...baseFlag,
targets: [{
values: ['user-key'],
variation: 0,
},
{
values: ['user-key2'],
variation: 2,
},
],
}, { key: 'user-key2' }, EvalResult.ForSuccess('two', Reasons.TargetMatch, 2)],
[{
...baseFlag,
targets: [{
values: ['user-key'],
variation: 0,
}],
}, { key: 'different-key' }, undefined],
[{
...baseFlag,
targets: [{
values: ['user-key'],
variation: 0,
},
{
values: ['user-key2'],
variation: 2,
},
],
contextTargets: [{
values: [],
variation: 2,
}],
}, { key: 'user-key2' }, EvalResult.ForSuccess('two', Reasons.TargetMatch, 2)],
[{
...baseFlag,
targets: [{
values: ['user-key'],
variation: 0,
},
{
values: ['user-key2'],
variation: 2,
},
],
contextTargets: [{
values: [],
variation: 2,
}],
}, { kind: 'org', key: 'user-key2' }, undefined],
[{
...baseFlag,
targets: [{
values: ['user-key'],
variation: 0,
},
{
values: ['user-key2'],
variation: 2,
},
],
contextTargets: [{
contextKind: 'org',
values: ['org-key'],
variation: 1,
}],
}, { kind: 'org', key: 'org-key' }, EvalResult.ForSuccess('one', Reasons.TargetMatch, 1)],
[{
...baseFlag,
contextTargets: [{
values: ['org-key'],
variation: 1,
}],
}, { key: 'user-key' }, undefined],
[{
...baseFlag,
}, { key: 'user-key' }, undefined],
])('given flag configurations with different targets', (flag, context, expected) => {
// @ts-ignore
it(`produces the expected evaluation result for context: ${context.key} ${context.kind} targets: ${flag.targets?.map((t) => `${t.values}, ${t.variation}`)} context targets: ${flag.contextTargets?.map((t) => `${t.contextKind}: ${t.values}, ${t.variation}`)}`, () => {
const result = evalTargets(flag, Context.FromLDContext(context)!);
expect(result?.isError).toEqual(expected?.isError);
expect(result?.detail).toStrictEqual(expected?.detail);
expect(result?.message).toEqual(expected?.message);
});
});
Loading