Skip to content

Commit 2db7fa8

Browse files
authored
fix: handling zero value responses (previously undefined) (#330)
Signed-off-by: odubajDT <[email protected]>
1 parent 97c999a commit 2db7fa8

File tree

3 files changed

+153
-60
lines changed

3 files changed

+153
-60
lines changed

libs/providers/flagd/src/lib/flagd-provider.spec.ts

+122-49
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,128 @@ describe(FlagdProvider.name, () => {
216216
});
217217
});
218218

219+
describe('basic flag resolution with zero values', () => {
220+
let client: Client;
221+
222+
// mock ServiceClient to inject
223+
const basicServiceClientMock: ServiceClient = {
224+
eventStream: jest.fn(() => {
225+
return {
226+
responses: {
227+
onComplete: jest.fn(() => {
228+
return;
229+
}),
230+
onError: jest.fn(() => {
231+
return;
232+
}),
233+
onMessage: jest.fn(() => {
234+
return;
235+
}),
236+
},
237+
};
238+
}),
239+
resolveBoolean: jest.fn((): UnaryCall<ResolveBooleanRequest, ResolveBooleanResponse> => {
240+
return Promise.resolve({
241+
request: {} as ResolveBooleanRequest,
242+
response: {
243+
variant: BOOLEAN_VARIANT,
244+
reason: REASON,
245+
},
246+
}) as unknown as UnaryCall<ResolveBooleanRequest, ResolveBooleanResponse>;
247+
}),
248+
resolveString: jest.fn((): UnaryCall<ResolveStringRequest, ResolveStringResponse> => {
249+
return Promise.resolve({
250+
request: {} as ResolveStringRequest,
251+
response: {
252+
variant: STRING_VARIANT,
253+
reason: REASON,
254+
} as ResolveStringResponse,
255+
}) as unknown as UnaryCall<ResolveStringRequest, ResolveStringResponse>;
256+
}),
257+
resolveFloat: jest.fn((): UnaryCall<ResolveFloatRequest, ResolveFloatResponse> => {
258+
return Promise.resolve({
259+
request: {} as ResolveFloatRequest,
260+
response: {
261+
variant: NUMBER_VARIANT,
262+
reason: REASON,
263+
} as ResolveFloatResponse,
264+
}) as unknown as UnaryCall<ResolveFloatRequest, ResolveFloatResponse>;
265+
}),
266+
resolveInt: jest.fn((): UnaryCall<ResolveIntRequest, ResolveIntResponse> => {
267+
throw new Error('resolveInt should not be called'); // we never call this method, we resolveFloat for all numbers.
268+
}),
269+
resolveObject: jest.fn((): UnaryCall<ResolveObjectRequest, ResolveObjectResponse> => {
270+
return Promise.resolve({
271+
request: {} as ResolveObjectRequest,
272+
response: {
273+
variant: OBJECT_VARIANT,
274+
reason: REASON,
275+
} as ResolveObjectResponse,
276+
}) as unknown as UnaryCall<ResolveObjectRequest, ResolveObjectResponse>;
277+
}),
278+
} as unknown as ServiceClient;
279+
280+
beforeEach(() => {
281+
// inject our mock GRPCService and ServiceClient
282+
OpenFeature.setProvider(
283+
new FlagdProvider(undefined, new GRPCService({ host: '', port: 123, tls: false }, basicServiceClientMock))
284+
);
285+
client = OpenFeature.getClient('test');
286+
});
287+
288+
describe(FlagdProvider.prototype.resolveBooleanEvaluation.name, () => {
289+
it(`should call ${ServiceClient.prototype.resolveBoolean} with key and context and return details`, async () => {
290+
const val = await client.getBooleanDetails(BOOLEAN_KEY, false, TEST_CONTEXT);
291+
expect(basicServiceClientMock.resolveBoolean).toHaveBeenCalledWith({
292+
flagKey: BOOLEAN_KEY,
293+
context: TEST_CONTEXT_CONVERTED,
294+
});
295+
expect(val.value).toEqual(false);
296+
expect(val.variant).toEqual(BOOLEAN_VARIANT);
297+
expect(val.reason).toEqual(REASON);
298+
});
299+
});
300+
301+
describe(FlagdProvider.prototype.resolveStringEvaluation.name, () => {
302+
it(`should call ${ServiceClient.prototype.resolveString} with key and context and return details`, async () => {
303+
const val = await client.getStringDetails(STRING_KEY, '', TEST_CONTEXT);
304+
expect(basicServiceClientMock.resolveString).toHaveBeenCalledWith({
305+
flagKey: STRING_KEY,
306+
context: TEST_CONTEXT_CONVERTED,
307+
});
308+
expect(val.value).toEqual('');
309+
expect(val.variant).toEqual(STRING_VARIANT);
310+
expect(val.reason).toEqual(REASON);
311+
});
312+
});
313+
314+
describe(FlagdProvider.prototype.resolveNumberEvaluation.name, () => {
315+
it(`should call ${ServiceClient.prototype.resolveFloat} with key and context and return details`, async () => {
316+
const val = await client.getNumberDetails(NUMBER_KEY, 0, TEST_CONTEXT);
317+
expect(basicServiceClientMock.resolveFloat).toHaveBeenCalledWith({
318+
flagKey: NUMBER_KEY,
319+
context: TEST_CONTEXT_CONVERTED,
320+
});
321+
expect(val.value).toEqual(0);
322+
expect(val.variant).toEqual(NUMBER_VARIANT);
323+
expect(val.reason).toEqual(REASON);
324+
});
325+
});
326+
327+
describe(FlagdProvider.prototype.resolveObjectEvaluation.name, () => {
328+
it(`should call ${ServiceClient.prototype.resolveObject} with key and context and return details`, async () => {
329+
const val = await client.getObjectDetails(OBJECT_KEY, {}, TEST_CONTEXT);
330+
expect(basicServiceClientMock.resolveObject).toHaveBeenCalledWith({
331+
flagKey: OBJECT_KEY,
332+
context: TEST_CONTEXT_CONVERTED,
333+
});
334+
expect(val.value).toEqual({});
335+
expect(val.variant).toEqual(OBJECT_VARIANT);
336+
expect(val.reason).toEqual(REASON);
337+
});
338+
});
339+
});
340+
219341
describe('caching', () => {
220342
const STATIC_BOOLEAN_KEY_1 = 'staticBoolflagOne';
221343
const STATIC_BOOLEAN_KEY_2 = 'staticBoolflagTwo';
@@ -544,53 +666,4 @@ describe(FlagdProvider.name, () => {
544666
});
545667
});
546668
});
547-
548-
describe('undefined object value', () => {
549-
let client: Client;
550-
551-
// mock ServiceClient to inject
552-
const undefinedObjectMock: ServiceClient = {
553-
eventStream: jest.fn(() => {
554-
return {
555-
responses: {
556-
onMessage: jest.fn(() => {
557-
return;
558-
}),
559-
},
560-
};
561-
}),
562-
resolveObject: jest.fn((): UnaryCall<ResolveObjectRequest, ResolveObjectResponse> => {
563-
return Promise.resolve({
564-
request: {} as ResolveObjectRequest,
565-
response: {
566-
value: undefined,
567-
reason: REASON,
568-
} as ResolveObjectResponse,
569-
}) as unknown as UnaryCall<ResolveObjectRequest, ResolveObjectResponse>;
570-
}),
571-
} as unknown as ServiceClient;
572-
573-
beforeEach(() => {
574-
// inject our mock GRPCService and ServiceClient
575-
OpenFeature.setProvider(
576-
new FlagdProvider(undefined, new GRPCService({ host: '', port: 123, tls: false }, undefinedObjectMock))
577-
);
578-
client = OpenFeature.getClient('test');
579-
});
580-
581-
describe(FlagdProvider.prototype.resolveObjectEvaluation.name, () => {
582-
const DEFAULT_INNER_KEY = 'some';
583-
const DEFAULT_INNER_VALUE = 'key';
584-
585-
it('should default and throw correct error', async () => {
586-
const val = await client.getObjectDetails(OBJECT_KEY, {
587-
[DEFAULT_INNER_KEY]: DEFAULT_INNER_VALUE,
588-
});
589-
expect(undefinedObjectMock.resolveObject).toHaveBeenCalled();
590-
expect(val.value).toEqual({ [DEFAULT_INNER_KEY]: DEFAULT_INNER_VALUE });
591-
expect(val.reason).toEqual(ERROR_REASON);
592-
expect(val.errorCode).toEqual(ErrorCode.PARSE_ERROR);
593-
});
594-
});
595-
});
596669
});

libs/providers/flagd/src/lib/service/grpc/service.ts

+28-8
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,15 @@ export class GRPCService implements Service {
114114
context: EvaluationContext,
115115
logger: Logger
116116
): Promise<ResolutionDetails<boolean>> {
117-
return this.resolve(this._client.resolveBoolean, flagKey, context, logger);
117+
return this.resolve(this._client.resolveBoolean, flagKey, context, logger, this.booleanParser);
118118
}
119119

120120
async resolveString(flagKey: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<string>> {
121-
return this.resolve(this._client.resolveString, flagKey, context, logger);
121+
return this.resolve(this._client.resolveString, flagKey, context, logger, this.stringParser);
122122
}
123123

124124
async resolveNumber(flagKey: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<number>> {
125-
return this.resolve(this._client.resolveFloat, flagKey, context, logger);
125+
return this.resolve(this._client.resolveFloat, flagKey, context, logger, this.numberParser);
126126
}
127127

128128
async resolveObject<T extends JsonValue>(
@@ -204,19 +204,39 @@ export class GRPCService implements Service {
204204
}
205205

206206
private objectParser = (struct: Struct) => {
207-
if (struct !== undefined) {
207+
if (struct) {
208208
return Struct.toJson(struct);
209-
} else {
210-
throw new ParseError('Object value undefined or missing.');
211209
}
210+
return {}
211+
};
212+
213+
private booleanParser = (value: boolean) => {
214+
if (value) {
215+
return value;
216+
}
217+
return false
218+
};
219+
220+
private stringParser = (value: string) => {
221+
if (value) {
222+
return value;
223+
}
224+
return ''
225+
};
226+
227+
private numberParser = (value: number) => {
228+
if (value) {
229+
return value;
230+
}
231+
return 0
212232
};
213233

214234
private async resolve<T extends FlagValue, Rq extends AnyRequest, Rs extends AnyResponse>(
215235
resolver: (request: AnyRequest) => UnaryCall<Rq, Rs>,
216236
flagKey: string,
217237
context: EvaluationContext,
218238
logger: Logger,
219-
parser?: (struct: Struct) => PbJsonValue
239+
parser?: (value: any) => any
220240
): Promise<ResolutionDetails<T>> {
221241
if (this._cacheActive) {
222242
const cached = this._cache?.get(flagKey);
@@ -232,7 +252,7 @@ export class GRPCService implements Service {
232252

233253
const resolved = {
234254
// invoke the parser method if passed
235-
value: parser ? parser.call(this, response.value as Struct) : response.value,
255+
value: parser ? parser.call(this, response.value) : response.value,
236256
reason: response.reason,
237257
variant: response.variant,
238258
} as ResolutionDetails<T>;

libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class GoFeatureFlagProvider implements Provider {
7171
}
7272

7373
/**
74-
* resolveBooleanEvaluation is calling the GO Feature Flag relay-proxy API and return a boolean value.
74+
* resolveStringEvaluation is calling the GO Feature Flag relay-proxy API and return a string value.
7575
* @param flagKey - name of your feature flag key.
7676
* @param defaultValue - default value is used if we are not able to evaluate the flag for this user.
7777
* @param context - the context used for flag evaluation.
@@ -96,7 +96,7 @@ export class GoFeatureFlagProvider implements Provider {
9696
}
9797

9898
/**
99-
* resolveBooleanEvaluation is calling the GO Feature Flag relay-proxy API and return a boolean value.
99+
* resolveNumberEvaluation is calling the GO Feature Flag relay-proxy API and return a number value.
100100
* @param flagKey - name of your feature flag key.
101101
* @param defaultValue - default value is used if we are not able to evaluate the flag for this user.
102102
* @param context - the context used for flag evaluation.
@@ -121,7 +121,7 @@ export class GoFeatureFlagProvider implements Provider {
121121
}
122122

123123
/**
124-
* resolveBooleanEvaluation is calling the GO Feature Flag relay-proxy API and return a boolean value.
124+
* resolveObjectEvaluation is calling the GO Feature Flag relay-proxy API and return an object.
125125
* @param flagKey - name of your feature flag key.
126126
* @param defaultValue - default value is used if we are not able to evaluate the flag for this user.
127127
* @param context - the context used for flag evaluation.

0 commit comments

Comments
 (0)