From 3ff536323ff4e330b900ca36157349daf258c71e Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 13 Mar 2023 16:47:30 -0400 Subject: [PATCH] DVC-6570 run all bucketing tests against variableForUser() and variableForUserPB() (#434) * run bucketing tests against variableForUser() and variableForUserPB() * cleanup --- .../__tests__/bucketing/bucketing.test.ts | 93 +++++++++---- .../__tests__/protobufVariableHelper.ts | 129 ++++++++++++++++++ .../__tests__/types/protobuf.test.ts | 2 +- .../assembly/index.ts | 1 - 4 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 lib/shared/bucketing-assembly-script/__tests__/protobufVariableHelper.ts diff --git a/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts b/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts index a782c70a9..fd52ae7b2 100644 --- a/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts +++ b/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts @@ -13,8 +13,12 @@ const { config, barrenConfig } = testData import moment from 'moment' import * as uuid from 'uuid' -import { BucketedUserConfig, SDKVariable } from '../../assembly/types' +import { + BucketedUserConfig, + SDKVariable, +} from '@devcycle/types' import { cleanupSDK, initSDK } from '../setPlatformData' +import { variableForUserPB, VariableForUserArgs } from '../protobufVariableHelper' type BoundedHash = { rolloutHash: number, bucketingHash: number } @@ -54,10 +58,25 @@ const generateBucketedConfig = (user: unknown): BucketedUserConfig => { return JSON.parse(bucketedConfig) as BucketedUserConfig } -const variableForUser = ( - { user, variableKey, variableType }: - { user: unknown, variableKey: string, variableType: VariableType } -): SDKVariable | null => { +const expectVariableForUser = ( + args: { user: any, variableKey: string, variableType: VariableType }, + expectedValue: unknown +) => { + const variable = variableForUser({ ...args, sdkKey }) + const pbVariable = variableForUserPB({ ...args, sdkKey }) + + if (expectedValue === null) { + expect(variable).toBeNull() + expect(pbVariable).toBeNull() + } else { + expect(variable).not.toBeNull() + expect(variable).toEqual(expectedValue) + expect(pbVariable).not.toBeNull() + expect(pbVariable).toEqual(expectedValue) + } +} + +const variableForUser = ({ user, variableKey, variableType }: VariableForUserArgs): SDKVariable | null => { const variableJSON = variableForUser_AS( sdkKey, JSON.stringify(user), variableKey, variableType, true ) @@ -222,8 +241,10 @@ describe('Config Parsing and Generating', () => { const c = generateBucketedConfig(user) expect(c).toEqual(expected) - expect(variableForUser({ user, variableKey: 'swagTest', variableType: VariableType.String })) - .toEqual(expected.variables.swagTest) + expectVariableForUser( + { user, variableKey: 'swagTest', variableType: VariableType.String }, + expected.variables.swagTest + ) }) it('puts the user in the target for the first audience they match', () => { @@ -372,16 +393,26 @@ describe('Config Parsing and Generating', () => { const c = generateBucketedConfig(user) expect(c).toEqual(expected) - expect(variableForUser({ user, variableKey: 'audience-match', variableType: VariableType.String })) - .toEqual(expected.variables['audience-match']) - expect(variableForUser({ user, variableKey: 'feature2.cool', variableType: VariableType.String })) - .toEqual(expected.variables['feature2.cool']) - expect(variableForUser({ user, variableKey: 'feature2.hello', variableType: VariableType.String })) - .toEqual(expected.variables['feature2.hello']) - expect(variableForUser({ user, variableKey: 'swagTest', variableType: VariableType.String })) - .toEqual(expected.variables['swagTest']) - expect(variableForUser({ user, variableKey: 'test', variableType: VariableType.String })) - .toEqual(expected.variables['test']) + expectVariableForUser( + { user, variableKey: 'audience-match', variableType: VariableType.String }, + expected.variables['audience-match'] + ) + expectVariableForUser( + { user, variableKey: 'feature2.cool', variableType: VariableType.String }, + expected.variables['feature2.cool'] + ) + expectVariableForUser( + { user, variableKey: 'feature2.hello', variableType: VariableType.String }, + expected.variables['feature2.hello'] + ) + expectVariableForUser( + { user, variableKey: 'swagTest', variableType: VariableType.String }, + expected.variables['swagTest'] + ) + expectVariableForUser( + { user, variableKey: 'test', variableType: VariableType.String }, + expected.variables['test'] + ) }) it('holds user back if not in rollout', () => { @@ -439,11 +470,14 @@ describe('Config Parsing and Generating', () => { } } initSDK(sdkKey, config) + const c = generateBucketedConfig(user) expect(c).toEqual(expected) - expect(variableForUser({ user, variableKey: 'feature2Var', variableType: VariableType.String })) - .toEqual(expected.variables['feature2Var']) + expectVariableForUser( + { user, variableKey: 'feature2Var', variableType: VariableType.String }, + expected.variables['feature2Var'] + ) }) it('puts user through if in rollout', () => { @@ -551,10 +585,14 @@ describe('Config Parsing and Generating', () => { const c = generateBucketedConfig(user) expect(c).toEqual(expected) - expect(variableForUser({ user, variableKey: 'swagTest', variableType: VariableType.String })) - .toEqual(expected.variables['swagTest']) - expect(variableForUser({ user, variableKey: 'feature2Var', variableType: VariableType.String })) - .toEqual(expected.variables['feature2Var']) + expectVariableForUser( + { user, variableKey: 'swagTest', variableType: VariableType.String }, + expected.variables['swagTest'] + ) + expectVariableForUser( + { user, variableKey: 'feature2Var', variableType: VariableType.String }, + expected.variables['feature2Var'] + ) }) it('errors when feature missing distribution', () => { @@ -567,8 +605,7 @@ describe('Config Parsing and Generating', () => { expect(() => generateBucketedConfig(user)) .toThrow('Failed to decide target variation: 61536f3bc838a705c105eb62') - expect(variableForUser({ user, variableKey: 'feature2Var', variableType: VariableType.String })) - .toBeNull() + expectVariableForUser({ user, variableKey: 'feature2Var', variableType: VariableType.String }, null) }) it('errors when config missing variations', () => { @@ -591,8 +628,7 @@ describe('Config Parsing and Generating', () => { expect(() => generateBucketedConfig(user)) .toThrow('Config missing variation: 615382338424cb11646d7667') - expect(variableForUser({ user, variableKey: 'feature2Var', variableType: VariableType.String })) - .toBeNull() + expectVariableForUser({ user, variableKey: 'feature2Var', variableType: VariableType.String }, null) }) it('errors when config missing variables', () => { @@ -605,8 +641,7 @@ describe('Config Parsing and Generating', () => { expect(() => generateBucketedConfig(user)) .toThrow('Config missing variable: 61538237b0a70b58ae6af71g') - expect(variableForUser({ user, variableKey: 'feature2.cool', variableType: VariableType.String })) - .toBeNull() + expectVariableForUser({ user, variableKey: 'feature2.cool', variableType: VariableType.String }, null) }) }) diff --git a/lib/shared/bucketing-assembly-script/__tests__/protobufVariableHelper.ts b/lib/shared/bucketing-assembly-script/__tests__/protobufVariableHelper.ts new file mode 100644 index 000000000..cb8114f3c --- /dev/null +++ b/lib/shared/bucketing-assembly-script/__tests__/protobufVariableHelper.ts @@ -0,0 +1,129 @@ +import path from 'path' +import protobuf from 'protobufjs' +import { SDKVariable, VariableType as VariableTypeStr } from '@devcycle/types' +import { + variableForUser_PB, VariableType +} from './bucketingImportHelper' + +// TODO replace all this PB importing + types once we import the pb file directly into static JS Classes +const protoFile = '../protobuf/variableForUserParams.proto' +const filePath = path.resolve(__dirname, protoFile) +const root = protobuf.loadSync(filePath) + +const VariableForUserParams_PB = root.lookupType('VariableForUserParams_PB') +if (!VariableForUserParams_PB) throw new Error('VariableForUserParams_PB not found') + +const SDKVariable_PB = root.lookupType('SDKVariable_PB') +if (!SDKVariable_PB) throw new Error('SDKVariable_PB not found') + +type SDKVariable_PB_Type = { + _id: string + type: number + key: string + boolValue: boolean + doubleValue: number + stringValue: string +} + +const pbSDKVariableToJS = (pbSDKVariable: SDKVariable_PB_Type): SDKVariable => { + if (pbSDKVariable.type === 0) { + return { + _id: pbSDKVariable._id, + key: pbSDKVariable.key, + value: pbSDKVariable.boolValue, + type: VariableTypeStr.boolean + } + } else if (pbSDKVariable.type === 1) { + return { + _id: pbSDKVariable._id, + key: pbSDKVariable.key, + value: pbSDKVariable.doubleValue, + type: VariableTypeStr.number + } + } else if (pbSDKVariable.type === 2) { + return { + _id: pbSDKVariable._id, + key: pbSDKVariable.key, + value: pbSDKVariable.stringValue, + type: VariableTypeStr.string + } + } else if (pbSDKVariable.type === 3) { + return { + _id: pbSDKVariable._id, + key: pbSDKVariable.key, + value: JSON.parse(pbSDKVariable.stringValue), + type: VariableTypeStr.json + } + } + throw new Error(`Unknown variable type: ${pbSDKVariable.type}`) +} + +enum CustomDataTypePB { + Bool, + Num, + Str, + Null +} + +type CustomDataValuePB = { + type: CustomDataTypePB, + boolValue?: boolean, + doubleValue?: number, + stringValue?: string +} + +const customDataToPB = (customData: any): Record | undefined => { + if (!customData) return undefined + + const customDataPB: Record = {} + for (const [key, value] of Object.entries(customData)) { + if (typeof value === 'boolean') { + customDataPB[key] = { type: CustomDataTypePB.Bool, boolValue: value } + } else if (typeof value === 'number') { + customDataPB[key] = { type: CustomDataTypePB.Num, doubleValue: value } + } else if (typeof value === 'string') { + customDataPB[key] = { type: CustomDataTypePB.Str, stringValue: value } + } else if (value === null) { + customDataPB[key] = { type: CustomDataTypePB.Null } + } else { + throw new Error(`Unknown custom data type: ${typeof value}`) + } + } + return customDataPB +} + +export type VariableForUserArgs = { sdkKey: string, user: any, variableKey: string, variableType: VariableType } + +export const variableForUserPB = ( + { sdkKey, user, variableKey, variableType }: VariableForUserArgs +): SDKVariable | null => { + const customData = customDataToPB(user.customData) + const privateCustomData = customDataToPB(user.privateCustomData) + const params = { + sdkKey, + variableKey, + variableType, + user: { + userId: user.user_id, + email: user.email ? { value: user.email, isNull: false } : undefined, + name: user.name ? { value: user.name, isNull: false } : undefined, + language: user.language ? { value: user.language, isNull: false } : undefined, + country: user.country ? { value: user.country, isNull: false } : undefined, + appBuild: user.appBuild ? { value: user.appBuild, isNull: false } : undefined, + appVersion: user.appVersion ? { value: user.appVersion, isNull: false } : undefined, + deviceModel: user.deviceModel ? { value: user.deviceModel, isNull: false } : undefined, + customData: customData ? { value: customData, isNull: false } : undefined, + privateCustomData: privateCustomData ? { value: privateCustomData, isNull: false } : undefined + }, + shouldTrackEvent: true + } + const err = VariableForUserParams_PB.verify(params) + if (err) throw new Error(err) + + const pbMsg = VariableForUserParams_PB.create(params) + const buffer = VariableForUserParams_PB.encode(pbMsg).finish() + const resultBuffer = variableForUser_PB(buffer) + if (!resultBuffer) return null + const pbSDKVariable = SDKVariable_PB.decode(resultBuffer!) as unknown as SDKVariable_PB_Type + return pbSDKVariableToJS(pbSDKVariable) +} diff --git a/lib/shared/bucketing-assembly-script/__tests__/types/protobuf.test.ts b/lib/shared/bucketing-assembly-script/__tests__/types/protobuf.test.ts index 937d9cb69..61fae9269 100644 --- a/lib/shared/bucketing-assembly-script/__tests__/types/protobuf.test.ts +++ b/lib/shared/bucketing-assembly-script/__tests__/types/protobuf.test.ts @@ -82,7 +82,7 @@ describe('protobuf variable tests', () => { }) const varForUserParams = { - sdkKey: sdkKey, + sdkKey, variableKey: 'swagTest', variableType: 2, shouldTrackEvent: true, diff --git a/lib/shared/bucketing-assembly-script/assembly/index.ts b/lib/shared/bucketing-assembly-script/assembly/index.ts index dc7d54929..04640a8a9 100644 --- a/lib/shared/bucketing-assembly-script/assembly/index.ts +++ b/lib/shared/bucketing-assembly-script/assembly/index.ts @@ -70,7 +70,6 @@ export function variableForUser_PB(protobuf: Uint8Array): Uint8Array | null { variableTypeFromPB(params.variableType), params.shouldTrackEvent ) - return variable ? variable.toProtobuf() : null }