Repository: open-feature/js-sdk
Pull request #752 (2024-01-10 18:30:31)

fix: wrong context passed to named providers
Fixed an issue with implementations of https://github.com/open-feature/js-sdk/pull/704 where the wrong context was passed to namespaced providers.

toddbaert: > Approving but I do think you should add an additional test. We're not sufficiently covering the evaluation process when using namespaced evaluation context.

Agreed I think it should be simple to add another.

Patch:
diff --git a/packages/client/src/client/open-feature-client.ts b/packages/client/src/client/open-feature-client.ts
index 84fdc79c..dc86f0a5 100644
--- a/packages/client/src/client/open-feature-client.ts
+++ b/packages/client/src/client/open-feature-client.ts
@@ -179,7 +179,7 @@ export class OpenFeatureClient implements Client {
     const allHooksReversed = [...allHooks].reverse();
 
     const context = {
-      ...OpenFeature.getContext(),
+      ...OpenFeature.getContext(this?.options?.name),
     };
 
     // this reference cannot change during the course of evaluation
diff --git a/packages/client/src/open-feature.ts b/packages/client/src/open-feature.ts
index 1aa50243..d86c2cc0 100644
--- a/packages/client/src/open-feature.ts
+++ b/packages/client/src/open-feature.ts
@@ -106,7 +106,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
    * @param {string} clientName The name to identify the client
    * @returns {EvaluationContext} Evaluation context
    */
-  getContext(clientName: string): EvaluationContext;
+  getContext(clientName?: string): EvaluationContext;
   getContext(nameOrUndefined?: string): EvaluationContext {
     const clientName = stringOrUndefined(nameOrUndefined);
     if (clientName) {


Test patch:
diff --git a/packages/client/test/evaluation-context.spec.ts b/packages/client/test/evaluation-context.spec.ts
index 823bd123..11892dda 100644
--- a/packages/client/test/evaluation-context.spec.ts
+++ b/packages/client/test/evaluation-context.spec.ts
@@ -12,9 +12,12 @@ class MockProvider implements Provider {
     return Promise.resolve();
   }
 
-  resolveBooleanEvaluation(): ResolutionDetails<boolean> {
-    throw new Error('Not implemented');
-  }
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  resolveBooleanEvaluation = jest.fn((flagKey: string, defaultValue: boolean, context: EvaluationContext) => {
+    return {
+      value: true
+    };
+  });
 
   resolveNumberEvaluation(): ResolutionDetails<number> {
     throw new Error('Not implemented');
@@ -76,6 +79,57 @@ describe('Evaluation Context', () => {
         expect(OpenFeature.getContext(clientName)).toEqual(globalContext);
       });
 
+      it('should call onContextChange for appropriate provider with appropriate context', async () => {
+        const globalContext: EvaluationContext = { scope: 'global' };
+        const testContext: EvaluationContext = { scope: 'test' };
+        const clientName = 'appropriateProviderTest';
+        const defaultProvider = new MockProvider();
+        const provider1 = new MockProvider();
+
+        await OpenFeature.setProviderAndWait(defaultProvider);
+        await OpenFeature.setProviderAndWait(clientName, provider1);
+
+        // Spy on context changed handlers of both providers
+        const defaultProviderSpy = jest.spyOn(defaultProvider, 'onContextChange');
+        const provider1Spy = jest.spyOn(provider1, 'onContextChange');
+
+        await OpenFeature.setContext(globalContext);
+        await OpenFeature.setContext(clientName, testContext);
+
+        // provider one should get global and specific context calls
+        expect(defaultProviderSpy).toHaveBeenCalledWith({}, globalContext);
+        expect(provider1Spy).toHaveBeenCalledWith(globalContext, testContext);
+      });
+
+      it('should pass correct context to resolver', async () => {
+        const globalContext: EvaluationContext = { scope: 'global' };
+        const testContext: EvaluationContext = { scope: 'test' };
+        const clientName = 'correctContextTest';
+        const defaultProvider = new MockProvider();
+        const provider1 = new MockProvider();
+
+        await OpenFeature.setProviderAndWait(defaultProvider);
+        await OpenFeature.setProviderAndWait(clientName, provider1);
+
+        // Spy on boolean resolvers of both providers
+        const defaultProviderSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
+        const provider1Spy = jest.spyOn(provider1, 'resolveBooleanEvaluation');
+
+        await OpenFeature.setContext(globalContext);
+        await OpenFeature.setContext(clientName, testContext);
+
+        const defaultClient = OpenFeature.getClient();
+        const provider1Client = OpenFeature.getClient(clientName);
+
+        const flagName = 'some-flag';
+        defaultClient.getBooleanValue(flagName, false);
+        provider1Client.getBooleanValue(flagName, false);
+
+        // provider one should get global and specific context
+        expect(defaultProviderSpy).toHaveBeenCalledWith(flagName, false, globalContext, expect.anything());
+        expect(provider1Spy).toHaveBeenCalledWith(flagName, false, testContext, expect.anything());
+      });
+
       it('should only call a providers onContextChange once when clearing context', async () => {
         const globalContext: EvaluationContext = { scope: 'global' };
         const testContext: EvaluationContext = { scope: 'test' };