Skip to content

Commit

Permalink
fix(web): only activate context for the current context
Browse files Browse the repository at this point in the history
check the context against the global context when activating the configuration

closes #6
  • Loading branch information
Billlynch committed Aug 3, 2023
1 parent 0fe4ea9 commit 96de81d
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 45 deletions.
2 changes: 1 addition & 1 deletion packages/openfeature-web-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ const provider = createConfidenceWebProvider({
fetchImplementation: window.fetch.bind(window),
});

OpenFeature.setProvider(provider);
await OpenFeature.setContext({
targetingKey: 'myTargetingKey',
});
OpenFeature.setProvider(provider);

const client = OpenFeature.getClient();
const result = client.getBooleanValue('flag.my-boolean', false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { OpenFeature, ProviderEvents } from '@openfeature/web-sdk';
import { OpenFeature } from '@openfeature/web-sdk';
import axios from 'axios';
import { createConfidenceWebProvider } from './factory';

describe('ConfidenceHTTPProvider E2E tests', () => {
describe('ConfidenceWebProvider E2E tests', () => {
beforeAll(() => {
const confidenceProvider = createConfidenceWebProvider({
fetchImplementation: async (url, request): Promise<Response> => {
Expand All @@ -18,19 +18,11 @@ describe('ConfidenceHTTPProvider E2E tests', () => {
region: 'eu',
clientSecret: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV',
});
const providerReadyPromise = new Promise<void>(resolve => {
OpenFeature.addHandler(ProviderEvents.Ready, () => {
resolve();
});
}).then(() => {
return OpenFeature.setContext({
targetingKey: 'test-a', // control
});
});

OpenFeature.setProvider(confidenceProvider);

return providerReadyPromise;
return OpenFeature.setContext({
targetingKey: 'test-a', // control
});
});

it('should resolve a boolean e2e', async () => {
Expand Down
73 changes: 62 additions & 11 deletions packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ErrorCode,
EvaluationContext,
Logger,
OpenFeature,
OpenFeatureAPI,
ProviderEvents,
ProviderStatus,
Expand All @@ -26,7 +27,7 @@ const mockClient = {
} as unknown as ConfidenceClient;

const dummyContext: ResolveContext = { targeting_key: 'test' };
const dummyEvaluationContext: EvaluationContext = { targetingKey: 'test' };
const dummyEvaluationContext: EvaluationContext = dummyContext;

const dummyConfiguration: Configuration = {
flags: {
Expand Down Expand Up @@ -112,12 +113,13 @@ describe('ConfidenceProvider', () => {
beforeEach(() => {
instanceUnderTest = new ConfidenceWebProvider(mockClient);
resolveMock.mockResolvedValue(dummyConfiguration);
jest.spyOn(OpenFeature, 'getContext').mockReturnValue(dummyContext);
});

describe('initialize', () => {
describe('with context', () => {
it('should resolve', async () => {
await instanceUnderTest.initialize({ targetingKey: 'test' });
await instanceUnderTest.initialize(dummyContext);

expect(resolveMock).toHaveBeenCalledWith(
{
Expand All @@ -132,7 +134,7 @@ describe('ConfidenceProvider', () => {
it('should change the provider status to READY', async () => {
expect(instanceUnderTest.status).toEqual(ProviderStatus.NOT_READY);

await instanceUnderTest.initialize({ targetingKey: 'test' });
await instanceUnderTest.initialize(dummyContext);

expect(instanceUnderTest.status).toEqual(ProviderStatus.READY);
});
Expand All @@ -141,7 +143,7 @@ describe('ConfidenceProvider', () => {
resolveMock.mockRejectedValue(new Error('something went wrong'));

try {
await instanceUnderTest.initialize({ targetingKey: 'test' });
await instanceUnderTest.initialize(dummyContext);
} catch (_) {
// do nothing
}
Expand All @@ -151,6 +153,10 @@ describe('ConfidenceProvider', () => {
});

describe('no context', () => {
beforeEach(() => {
jest.spyOn(OpenFeature, 'getContext').mockReturnValue({});
});

it('should change the provider status to READY', async () => {
expect(instanceUnderTest.status).toEqual(ProviderStatus.NOT_READY);

Expand All @@ -161,6 +167,7 @@ describe('ConfidenceProvider', () => {
});

it('should change the provider status with context', async () => {
jest.spyOn(OpenFeature, 'getContext').mockReturnValue({});
expect(instanceUnderTest.status).toEqual(ProviderStatus.NOT_READY);

await instanceUnderTest.initialize();
Expand All @@ -179,7 +186,6 @@ describe('ConfidenceProvider', () => {
},
{
flags: [],
apply: false,
},
);
});
Expand All @@ -190,8 +196,46 @@ describe('ConfidenceProvider', () => {
expect(resolveMock).not.toHaveBeenCalled();
});

it('should only emit ready and change the status when the context has not been changed mid change', async () => {
const staleHandler = jest.fn();
const readyHandler = jest.fn();
instanceUnderTest.events.addHandler(ProviderEvents.Stale, staleHandler);
instanceUnderTest.events.addHandler(ProviderEvents.Ready, readyHandler);
const getContextMock = jest.spyOn(OpenFeature, 'getContext');
let firstResolve: (() => void) | undefined;
let secondResolve: (() => void) | undefined;

resolveMock
.mockReturnValueOnce(
new Promise<void>(res => {
firstResolve = res;
}),
)
.mockReturnValueOnce(
new Promise<void>(res => {
secondResolve = res;
}),
);

getContextMock.mockReturnValue({ targetingKey: 'a' });
const firstSet = instanceUnderTest.onContextChange(dummyContext, { targetingKey: 'a' });
getContextMock.mockReturnValue({ targetingKey: 'b' });
const secondSet = instanceUnderTest.onContextChange(dummyContext, { targetingKey: 'b' });

firstResolve?.();
await firstSet;
expect(readyHandler).toHaveBeenCalledTimes(0);

secondResolve?.();
await secondSet;
expect(readyHandler).toHaveBeenCalledTimes(1);

expect(staleHandler).toHaveBeenCalledTimes(2);
expect(staleHandler).toHaveBeenCalledBefore(readyHandler);
});

it('should return default with reason stale during fetch', async () => {
await instanceUnderTest.initialize({ targetingKey: 'A' });
await instanceUnderTest.initialize(dummyContext);

expect(
instanceUnderTest.resolveBooleanEvaluation('testFlag.bool', false, { targetingKey: 'B' }, dummyConsole),
Expand All @@ -208,6 +252,7 @@ describe('ConfidenceProvider', () => {
const readyHandler = jest.fn();
instanceUnderTest.events.addHandler(ProviderEvents.Stale, staleHandler);
instanceUnderTest.events.addHandler(ProviderEvents.Ready, readyHandler);
jest.spyOn(OpenFeature, 'getContext').mockReturnValue({ targetingKey: 'a' });

await instanceUnderTest.onContextChange(dummyContext, { targetingKey: 'a' });

Expand Down Expand Up @@ -695,10 +740,12 @@ describe('events', () => {
const staleHandler = jest.fn();
let initPromise: Promise<void> | undefined;

afterEach(() => {
afterEach(async () => {
openFeatureAPI.removeHandler(ProviderEvents.Error, errorHandler);
openFeatureAPI.removeHandler(ProviderEvents.Ready, readyHandler);
openFeatureAPI.removeHandler(ProviderEvents.Stale, staleHandler);
openFeatureAPI.setLogger(dummyConsole);
return openFeatureAPI.setContext({});
});

beforeEach(() => {
Expand All @@ -718,26 +765,30 @@ describe('events', () => {
it('should emit ready stale ready on successful initialisation and context change', async () => {
resolveMock.mockResolvedValue(dummyConfiguration);
openFeatureAPI.setProvider(new ConfidenceWebProvider(mockClient));
jest.spyOn(OpenFeature, 'getContext').mockReturnValue({ targetingKey: 'user-a' });
await initPromise;
await openFeatureAPI.setContext({ targetingKey: 'user-a', name: 'Kurt' });
jest.spyOn(OpenFeature, 'getContext').mockReturnValue({ targetingKey: 'user-b' });
await openFeatureAPI.setContext({ targetingKey: 'user-b' });

expect(readyHandler).toHaveBeenCalledTimes(3);
expect(readyHandler).toHaveBeenCalledTimes(2);
expect(staleHandler).toHaveBeenCalledTimes(1);
expect(errorHandler).toHaveBeenCalledTimes(0);
});

it('should emit error stale error on failed initialisation and context change', async () => {
resolveMock.mockRejectedValue(new Error('some error'));
openFeatureAPI.setProvider(new ConfidenceWebProvider(mockClient));
jest.spyOn(OpenFeature, 'getContext').mockReturnValue({ targetingKey: 'user-a' });
await initPromise;
try {
await openFeatureAPI.setContext({ targetingKey: 'user-a' });
jest.spyOn(OpenFeature, 'getContext').mockReturnValue({ targetingKey: 'user-b' });
await openFeatureAPI.setContext({ targetingKey: 'user-b' });
} catch (_) {
// do nothing
}

expect(readyHandler).toHaveBeenCalledTimes(0);
expect(staleHandler).toHaveBeenCalledTimes(1);
expect(errorHandler).toHaveBeenCalledTimes(3);
expect(errorHandler).toHaveBeenCalledTimes(2);
});
});
39 changes: 19 additions & 20 deletions packages/openfeature-web-provider/src/ConfidenceWebProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
EvaluationContext,
JsonValue,
Logger,
OpenFeature,
OpenFeatureEventEmitter,
Provider,
ProviderEvents,
Expand Down Expand Up @@ -37,37 +38,35 @@ export class ConfidenceWebProvider implements Provider {
}

async initialize(context?: EvaluationContext): Promise<void> {
try {
this.configuration = await this.client.resolve(this.convertContext(context || {}), {
flags: [],
});
this.status = ProviderStatus.READY;
// this event should be emitted by the OpenFeature sdk onto the client handlers, but in current version is not work.
this.events.emit(ProviderEvents.Ready);
return Promise.resolve();
} catch (e) {
this.status = ProviderStatus.ERROR;
// this event should be emitted by the OpenFeature sdk onto the client handlers, but in current version is not work.
this.events.emit(ProviderEvents.Error);
throw e;
}
await this.fetchNewConfiguration(context || {});
}

async onContextChange(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {
if (equal(oldContext, newContext)) {
return;
}
this.events.emit(ProviderEvents.Stale);
await this.fetchNewConfiguration(newContext);
}

private async fetchNewConfiguration(context: EvaluationContext): Promise<void> {
try {
this.configuration = await this.client.resolve(this.convertContext(newContext || {}), {
apply: false,
const config = await this.client.resolve(this.convertContext(context), {
flags: [],
});
this.status = ProviderStatus.READY;
this.events.emit(ProviderEvents.Ready);
const oldCtx = OpenFeature.getContext();
if (equal(oldCtx, context)) {
this.configuration = config;
this.status = ProviderStatus.READY;
this.events.emit(ProviderEvents.Ready);
}
return Promise.resolve();
} catch (e) {
this.status = ProviderStatus.ERROR;
this.events.emit(ProviderEvents.Error);
if (equal(OpenFeature.getContext(), context)) {
this.status = ProviderStatus.ERROR;
this.events.emit(ProviderEvents.Error);
}
throw e;
}
}

Expand Down

0 comments on commit 96de81d

Please sign in to comment.