Skip to content

Commit d1f5049

Browse files
beeme1mrtoddbaert
andauthored
feat: add support for a blocking setProvider (#577)
Signed-off-by: Michael Beemer <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 9dd2d38 commit d1f5049

File tree

7 files changed

+197
-37
lines changed

7 files changed

+197
-37
lines changed

packages/client/test/client.spec.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
OpenFeature,
1010
OpenFeatureClient,
1111
Provider,
12+
ProviderStatus,
1213
ResolutionDetails,
1314
StandardResolutionReasons,
1415
} from '../src';
@@ -91,14 +92,73 @@ const MOCK_PROVIDER: Provider = {
9192
};
9293

9394
describe('OpenFeatureClient', () => {
94-
beforeAll(() => {
95+
beforeEach(() => {
9596
OpenFeature.setProvider(MOCK_PROVIDER);
9697
});
9798

9899
afterEach(() => {
99100
jest.clearAllMocks();
100101
});
101102

103+
describe('Requirement 1.1.8', () => {
104+
class mockAsyncProvider implements Provider {
105+
metadata = {
106+
name: 'mock-async',
107+
};
108+
109+
status = ProviderStatus.NOT_READY;
110+
readonly runsOn = 'client';
111+
112+
constructor(private readonly throwInInit: boolean) {}
113+
114+
async initialize(): Promise<void> {
115+
if (this.throwInInit) {
116+
try {
117+
throw new Error('provider failed to initialize');
118+
} catch (err) {
119+
this.status = ProviderStatus.ERROR;
120+
throw err;
121+
}
122+
}
123+
this.status = ProviderStatus.READY;
124+
return;
125+
}
126+
127+
resolveBooleanEvaluation(): ResolutionDetails<boolean> {
128+
throw new Error('Method not implemented.');
129+
}
130+
resolveStringEvaluation(): ResolutionDetails<string> {
131+
throw new Error('Method not implemented.');
132+
}
133+
resolveNumberEvaluation(): ResolutionDetails<number> {
134+
throw new Error('Method not implemented.');
135+
}
136+
resolveObjectEvaluation<T extends JsonValue>(): ResolutionDetails<T> {
137+
throw new Error('Method not implemented.');
138+
}
139+
}
140+
141+
it('should wait for the provider to successfully initialize', async () => {
142+
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');
143+
144+
const provider = new mockAsyncProvider(false);
145+
expect(provider.status).toBe(ProviderStatus.NOT_READY);
146+
await OpenFeature.setProviderAndWait(provider);
147+
expect(provider.status).toBe(ProviderStatus.READY);
148+
expect(spy).toBeCalled();
149+
});
150+
151+
it('should wait for the provider to fail during initialization', async () => {
152+
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');
153+
154+
const provider = new mockAsyncProvider(true);
155+
expect(provider.status).toBe(ProviderStatus.NOT_READY);
156+
await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow();
157+
expect(provider.status).toBe(ProviderStatus.ERROR);
158+
expect(spy).toBeCalled();
159+
});
160+
});
161+
102162
describe('Requirement 1.2.1', () => {
103163
it('should allow addition of hooks', () => {
104164
expect(OpenFeatureClient.prototype.addHooks).toBeDefined();

packages/client/test/events.spec.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const TIMEOUT = 1000;
1717
class MockProvider implements Provider {
1818
readonly metadata: ProviderMetadata;
1919
readonly events?: OpenFeatureEventEmitter;
20+
readonly runsOn = 'client';
2021
private hasInitialize: boolean;
2122
private failOnInit: boolean;
2223
private initDelay?: number;
@@ -85,7 +86,8 @@ describe('Events', () => {
8586
/* eslint-disable @typescript-eslint/no-explicit-any */
8687
(OpenFeature as any)._clientEventHandlers = new Map();
8788
/* eslint-disable @typescript-eslint/no-explicit-any */
88-
(OpenFeature as any)._clientEvents = new Map(); });
89+
(OpenFeature as any)._clientEvents = new Map();
90+
});
8991

9092
beforeEach(() => {
9193
OpenFeature.setProvider(NOOP_PROVIDER);
@@ -178,7 +180,7 @@ describe('Events', () => {
178180
OpenFeature.addHandler(ProviderEvents.Error, () => {
179181
resolve();
180182
});
181-
})
183+
}),
182184
]).then(() => {
183185
done();
184186
});
@@ -306,7 +308,11 @@ describe('Events', () => {
306308
});
307309

308310
it('handler added while while provider initializing runs', (done) => {
309-
const provider = new MockProvider({ name: 'race', initialStatus: ProviderStatus.NOT_READY, initDelay: TIMEOUT / 2 });
311+
const provider = new MockProvider({
312+
name: 'race',
313+
initialStatus: ProviderStatus.NOT_READY,
314+
initDelay: TIMEOUT / 2,
315+
});
310316

311317
// set the default provider
312318
OpenFeature.setProvider(provider);
@@ -499,12 +505,12 @@ describe('Events', () => {
499505
describe('API', () => {
500506
it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
501507
const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });
502-
508+
503509
OpenFeature.setProvider(clientId, provider);
504510
expect(provider.initialize).not.toHaveBeenCalled();
505-
511+
506512
OpenFeature.addHandler(ProviderEvents.Error, () => {
507-
done();
513+
done();
508514
});
509515
});
510516
});
@@ -513,14 +519,14 @@ describe('Events', () => {
513519
it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
514520
const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
515521
const client = OpenFeature.getClient(clientId);
516-
522+
517523
OpenFeature.setProvider(clientId, provider);
518524
expect(provider.initialize).not.toHaveBeenCalled();
519-
525+
520526
client.addHandler(ProviderEvents.Ready, () => {
521-
done();
527+
done();
522528
});
523529
});
524-
});
530+
});
525531
});
526532
});

packages/client/test/open-feature.spec.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { Paradigm } from '@openfeature/shared';
22
import { OpenFeature, OpenFeatureAPI, OpenFeatureClient, Provider, ProviderStatus } from '../src';
33

4-
const mockProvider = (config?: {
5-
initialStatus?: ProviderStatus,
6-
runsOn?: Paradigm,
7-
}) => {
4+
const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => {
85
return {
96
metadata: {
107
name: 'mock-events-success',
118
},
12-
runsOn: config?.runsOn,
9+
runsOn: config?.runsOn || 'client',
1310
status: config?.initialStatus || ProviderStatus.NOT_READY,
1411
initialize: jest.fn(() => {
1512
return Promise.resolve('started');
@@ -40,13 +37,13 @@ describe('OpenFeature', () => {
4037

4138
describe('Requirement 1.1.2.1', () => {
4239
it('should throw because the provider is not intended for the client', () => {
43-
const provider = mockProvider({ runsOn: 'server'});
40+
const provider = mockProvider({ runsOn: 'server' });
4441
expect(() => OpenFeature.setProvider(provider)).toThrowError(
4542
"Provider 'mock-events-success' is intended for use on the server."
4643
);
4744
});
4845
it('should succeed because the provider is intended for the client', () => {
49-
const provider = mockProvider({ runsOn: 'client'});
46+
const provider = mockProvider({ runsOn: 'client' });
5047
expect(() => OpenFeature.setProvider(provider)).not.toThrowError();
5148
});
5249
});
@@ -59,7 +56,7 @@ describe('OpenFeature', () => {
5956
expect(provider.initialize).toHaveBeenCalled();
6057
});
6158

62-
it('should not invoke initialze function if the provider is not in state NOT_READY', () => {
59+
it('should not invoke initialize function if the provider is not in state NOT_READY', () => {
6360
const provider = mockProvider({ initialStatus: ProviderStatus.READY });
6461
OpenFeature.setProvider(provider);
6562
expect(OpenFeature.providerMetadata.name).toBe('mock-events-success');

packages/server/test/client.spec.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
OpenFeature,
1111
OpenFeatureClient,
1212
Provider,
13+
ProviderStatus,
1314
ResolutionDetails,
1415
StandardResolutionReasons,
1516
TransactionContext,
@@ -82,14 +83,73 @@ const MOCK_PROVIDER: Provider = {
8283
};
8384

8485
describe('OpenFeatureClient', () => {
85-
beforeAll(() => {
86+
beforeEach(() => {
8687
OpenFeature.setProvider(MOCK_PROVIDER);
8788
});
8889

8990
afterEach(() => {
9091
jest.clearAllMocks();
9192
});
9293

94+
describe('Requirement 1.1.8', () => {
95+
class mockAsyncProvider implements Provider {
96+
metadata = {
97+
name: 'mock-async',
98+
};
99+
100+
status = ProviderStatus.NOT_READY;
101+
readonly runsOn = 'server';
102+
103+
constructor(private readonly throwInInit: boolean) {}
104+
105+
async initialize(): Promise<void> {
106+
if (this.throwInInit) {
107+
try {
108+
throw new Error('provider failed to initialize');
109+
} catch (err) {
110+
this.status = ProviderStatus.ERROR;
111+
throw err;
112+
}
113+
}
114+
this.status = ProviderStatus.READY;
115+
return;
116+
}
117+
118+
resolveBooleanEvaluation(): Promise<ResolutionDetails<boolean>> {
119+
throw new Error('Method not implemented.');
120+
}
121+
resolveStringEvaluation(): Promise<ResolutionDetails<string>> {
122+
throw new Error('Method not implemented.');
123+
}
124+
resolveNumberEvaluation(): Promise<ResolutionDetails<number>> {
125+
throw new Error('Method not implemented.');
126+
}
127+
resolveObjectEvaluation<T extends JsonValue>(): Promise<ResolutionDetails<T>> {
128+
throw new Error('Method not implemented.');
129+
}
130+
}
131+
132+
it('should wait for the provider to successfully initialize', async () => {
133+
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');
134+
135+
const provider = new mockAsyncProvider(false);
136+
expect(provider.status).toBe(ProviderStatus.NOT_READY);
137+
await OpenFeature.setProviderAndWait(provider);
138+
expect(provider.status).toBe(ProviderStatus.READY);
139+
expect(spy).toBeCalled();
140+
});
141+
142+
it('should wait for the provider to fail during initialization', async () => {
143+
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');
144+
145+
const provider = new mockAsyncProvider(true);
146+
expect(provider.status).toBe(ProviderStatus.NOT_READY);
147+
await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow();
148+
expect(provider.status).toBe(ProviderStatus.ERROR);
149+
expect(spy).toBeCalled();
150+
});
151+
});
152+
93153
describe('Requirement 1.2.1', () => {
94154
it('should allow addition of hooks', () => {
95155
expect(OpenFeatureClient.prototype.addHooks).toBeDefined();
@@ -358,7 +418,7 @@ describe('OpenFeatureClient', () => {
358418
const defaultNumberValue = 123;
359419
const defaultStringValue = 'hey!';
360420

361-
beforeAll(async () => {
421+
beforeEach(async () => {
362422
OpenFeature.setProvider(errorProvider);
363423
client = OpenFeature.getClient();
364424
nonOpenFeatureErrorDetails = await client.getNumberDetails('some-flag', defaultNumberValue);

packages/server/test/events.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const TIMEOUT = 1000;
1717
class MockProvider implements Provider {
1818
readonly metadata: ProviderMetadata;
1919
readonly events?: OpenFeatureEventEmitter;
20+
readonly runsOn = 'server';
2021
private hasInitialize: boolean;
2122
private failOnInit: boolean;
2223
private initDelay?: number;

packages/server/test/open-feature.spec.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { Paradigm } from '@openfeature/shared';
22
import { OpenFeature, OpenFeatureAPI, OpenFeatureClient, Provider, ProviderStatus } from '../src';
33

4-
const mockProvider = (config?: {
5-
initialStatus?: ProviderStatus,
6-
runsOn?: Paradigm,
7-
}) => {
4+
const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => {
85
return {
96
metadata: {
107
name: 'mock-events-success',
118
},
12-
runsOn: config?.runsOn,
9+
runsOn: config?.runsOn || 'server',
1310
status: config?.initialStatus || ProviderStatus.NOT_READY,
1411
initialize: jest.fn(() => {
1512
return Promise.resolve('started');
@@ -40,13 +37,13 @@ describe('OpenFeature', () => {
4037

4138
describe('Requirement 1.1.2.1', () => {
4239
it('should throw because the provider is not intended for the server', () => {
43-
const provider = mockProvider({ runsOn: 'client'});
40+
const provider = mockProvider({ runsOn: 'client' });
4441
expect(() => OpenFeature.setProvider(provider)).toThrowError(
4542
"Provider 'mock-events-success' is intended for use on the client."
4643
);
4744
});
4845
it('should succeed because the provider is intended for the server', () => {
49-
const provider = mockProvider({ runsOn: 'server'});
46+
const provider = mockProvider({ runsOn: 'server' });
5047
expect(() => OpenFeature.setProvider(provider)).not.toThrowError();
5148
});
5249
});
@@ -59,7 +56,7 @@ describe('OpenFeature', () => {
5956
expect(provider.initialize).toHaveBeenCalled();
6057
});
6158

62-
it('should not invoke initialze function if the provider is not in state NOT_READY', () => {
59+
it('should not invoke initialize function if the provider is not in state NOT_READY', () => {
6360
const provider = mockProvider({ initialStatus: ProviderStatus.READY });
6461
OpenFeature.setProvider(provider);
6562
expect(OpenFeature.providerMetadata.name).toBe('mock-events-success');

0 commit comments

Comments
 (0)