Skip to content

Commit

Permalink
feat: add support for a blocking setProvider (#577)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Beemer <[email protected]>
Co-authored-by: Todd Baert <[email protected]>
  • Loading branch information
beeme1mr and toddbaert authored Oct 9, 2023
1 parent 9dd2d38 commit d1f5049
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 37 deletions.
62 changes: 61 additions & 1 deletion packages/client/test/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
OpenFeature,
OpenFeatureClient,
Provider,
ProviderStatus,
ResolutionDetails,
StandardResolutionReasons,
} from '../src';
Expand Down Expand Up @@ -91,14 +92,73 @@ const MOCK_PROVIDER: Provider = {
};

describe('OpenFeatureClient', () => {
beforeAll(() => {
beforeEach(() => {
OpenFeature.setProvider(MOCK_PROVIDER);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('Requirement 1.1.8', () => {
class mockAsyncProvider implements Provider {
metadata = {
name: 'mock-async',
};

status = ProviderStatus.NOT_READY;
readonly runsOn = 'client';

constructor(private readonly throwInInit: boolean) {}

async initialize(): Promise<void> {
if (this.throwInInit) {
try {
throw new Error('provider failed to initialize');
} catch (err) {
this.status = ProviderStatus.ERROR;
throw err;
}
}
this.status = ProviderStatus.READY;
return;
}

resolveBooleanEvaluation(): ResolutionDetails<boolean> {
throw new Error('Method not implemented.');
}
resolveStringEvaluation(): ResolutionDetails<string> {
throw new Error('Method not implemented.');
}
resolveNumberEvaluation(): ResolutionDetails<number> {
throw new Error('Method not implemented.');
}
resolveObjectEvaluation<T extends JsonValue>(): ResolutionDetails<T> {
throw new Error('Method not implemented.');
}
}

it('should wait for the provider to successfully initialize', async () => {
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');

const provider = new mockAsyncProvider(false);
expect(provider.status).toBe(ProviderStatus.NOT_READY);
await OpenFeature.setProviderAndWait(provider);
expect(provider.status).toBe(ProviderStatus.READY);
expect(spy).toBeCalled();
});

it('should wait for the provider to fail during initialization', async () => {
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');

const provider = new mockAsyncProvider(true);
expect(provider.status).toBe(ProviderStatus.NOT_READY);
await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow();
expect(provider.status).toBe(ProviderStatus.ERROR);
expect(spy).toBeCalled();
});
});

describe('Requirement 1.2.1', () => {
it('should allow addition of hooks', () => {
expect(OpenFeatureClient.prototype.addHooks).toBeDefined();
Expand Down
26 changes: 16 additions & 10 deletions packages/client/test/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const TIMEOUT = 1000;
class MockProvider implements Provider {
readonly metadata: ProviderMetadata;
readonly events?: OpenFeatureEventEmitter;
readonly runsOn = 'client';
private hasInitialize: boolean;
private failOnInit: boolean;
private initDelay?: number;
Expand Down Expand Up @@ -85,7 +86,8 @@ describe('Events', () => {
/* eslint-disable @typescript-eslint/no-explicit-any */
(OpenFeature as any)._clientEventHandlers = new Map();
/* eslint-disable @typescript-eslint/no-explicit-any */
(OpenFeature as any)._clientEvents = new Map(); });
(OpenFeature as any)._clientEvents = new Map();
});

beforeEach(() => {
OpenFeature.setProvider(NOOP_PROVIDER);
Expand Down Expand Up @@ -178,7 +180,7 @@ describe('Events', () => {
OpenFeature.addHandler(ProviderEvents.Error, () => {
resolve();
});
})
}),
]).then(() => {
done();
});
Expand Down Expand Up @@ -306,7 +308,11 @@ describe('Events', () => {
});

it('handler added while while provider initializing runs', (done) => {
const provider = new MockProvider({ name: 'race', initialStatus: ProviderStatus.NOT_READY, initDelay: TIMEOUT / 2 });
const provider = new MockProvider({
name: 'race',
initialStatus: ProviderStatus.NOT_READY,
initDelay: TIMEOUT / 2,
});

// set the default provider
OpenFeature.setProvider(provider);
Expand Down Expand Up @@ -499,12 +505,12 @@ describe('Events', () => {
describe('API', () => {
it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });

OpenFeature.setProvider(clientId, provider);
expect(provider.initialize).not.toHaveBeenCalled();

OpenFeature.addHandler(ProviderEvents.Error, () => {
done();
done();
});
});
});
Expand All @@ -513,14 +519,14 @@ describe('Events', () => {
it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
const client = OpenFeature.getClient(clientId);

OpenFeature.setProvider(clientId, provider);
expect(provider.initialize).not.toHaveBeenCalled();

client.addHandler(ProviderEvents.Ready, () => {
done();
done();
});
});
});
});
});
});
13 changes: 5 additions & 8 deletions packages/client/test/open-feature.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { Paradigm } from '@openfeature/shared';
import { OpenFeature, OpenFeatureAPI, OpenFeatureClient, Provider, ProviderStatus } from '../src';

const mockProvider = (config?: {
initialStatus?: ProviderStatus,
runsOn?: Paradigm,
}) => {
const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => {
return {
metadata: {
name: 'mock-events-success',
},
runsOn: config?.runsOn,
runsOn: config?.runsOn || 'client',
status: config?.initialStatus || ProviderStatus.NOT_READY,
initialize: jest.fn(() => {
return Promise.resolve('started');
Expand Down Expand Up @@ -40,13 +37,13 @@ describe('OpenFeature', () => {

describe('Requirement 1.1.2.1', () => {
it('should throw because the provider is not intended for the client', () => {
const provider = mockProvider({ runsOn: 'server'});
const provider = mockProvider({ runsOn: 'server' });
expect(() => OpenFeature.setProvider(provider)).toThrowError(
"Provider 'mock-events-success' is intended for use on the server."
);
});
it('should succeed because the provider is intended for the client', () => {
const provider = mockProvider({ runsOn: 'client'});
const provider = mockProvider({ runsOn: 'client' });
expect(() => OpenFeature.setProvider(provider)).not.toThrowError();
});
});
Expand All @@ -59,7 +56,7 @@ describe('OpenFeature', () => {
expect(provider.initialize).toHaveBeenCalled();
});

it('should not invoke initialze function if the provider is not in state NOT_READY', () => {
it('should not invoke initialize function if the provider is not in state NOT_READY', () => {
const provider = mockProvider({ initialStatus: ProviderStatus.READY });
OpenFeature.setProvider(provider);
expect(OpenFeature.providerMetadata.name).toBe('mock-events-success');
Expand Down
64 changes: 62 additions & 2 deletions packages/server/test/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
OpenFeature,
OpenFeatureClient,
Provider,
ProviderStatus,
ResolutionDetails,
StandardResolutionReasons,
TransactionContext,
Expand Down Expand Up @@ -82,14 +83,73 @@ const MOCK_PROVIDER: Provider = {
};

describe('OpenFeatureClient', () => {
beforeAll(() => {
beforeEach(() => {
OpenFeature.setProvider(MOCK_PROVIDER);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('Requirement 1.1.8', () => {
class mockAsyncProvider implements Provider {
metadata = {
name: 'mock-async',
};

status = ProviderStatus.NOT_READY;
readonly runsOn = 'server';

constructor(private readonly throwInInit: boolean) {}

async initialize(): Promise<void> {
if (this.throwInInit) {
try {
throw new Error('provider failed to initialize');
} catch (err) {
this.status = ProviderStatus.ERROR;
throw err;
}
}
this.status = ProviderStatus.READY;
return;
}

resolveBooleanEvaluation(): Promise<ResolutionDetails<boolean>> {
throw new Error('Method not implemented.');
}
resolveStringEvaluation(): Promise<ResolutionDetails<string>> {
throw new Error('Method not implemented.');
}
resolveNumberEvaluation(): Promise<ResolutionDetails<number>> {
throw new Error('Method not implemented.');
}
resolveObjectEvaluation<T extends JsonValue>(): Promise<ResolutionDetails<T>> {
throw new Error('Method not implemented.');
}
}

it('should wait for the provider to successfully initialize', async () => {
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');

const provider = new mockAsyncProvider(false);
expect(provider.status).toBe(ProviderStatus.NOT_READY);
await OpenFeature.setProviderAndWait(provider);
expect(provider.status).toBe(ProviderStatus.READY);
expect(spy).toBeCalled();
});

it('should wait for the provider to fail during initialization', async () => {
const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize');

const provider = new mockAsyncProvider(true);
expect(provider.status).toBe(ProviderStatus.NOT_READY);
await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow();
expect(provider.status).toBe(ProviderStatus.ERROR);
expect(spy).toBeCalled();
});
});

describe('Requirement 1.2.1', () => {
it('should allow addition of hooks', () => {
expect(OpenFeatureClient.prototype.addHooks).toBeDefined();
Expand Down Expand Up @@ -358,7 +418,7 @@ describe('OpenFeatureClient', () => {
const defaultNumberValue = 123;
const defaultStringValue = 'hey!';

beforeAll(async () => {
beforeEach(async () => {
OpenFeature.setProvider(errorProvider);
client = OpenFeature.getClient();
nonOpenFeatureErrorDetails = await client.getNumberDetails('some-flag', defaultNumberValue);
Expand Down
1 change: 1 addition & 0 deletions packages/server/test/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const TIMEOUT = 1000;
class MockProvider implements Provider {
readonly metadata: ProviderMetadata;
readonly events?: OpenFeatureEventEmitter;
readonly runsOn = 'server';
private hasInitialize: boolean;
private failOnInit: boolean;
private initDelay?: number;
Expand Down
13 changes: 5 additions & 8 deletions packages/server/test/open-feature.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { Paradigm } from '@openfeature/shared';
import { OpenFeature, OpenFeatureAPI, OpenFeatureClient, Provider, ProviderStatus } from '../src';

const mockProvider = (config?: {
initialStatus?: ProviderStatus,
runsOn?: Paradigm,
}) => {
const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => {
return {
metadata: {
name: 'mock-events-success',
},
runsOn: config?.runsOn,
runsOn: config?.runsOn || 'server',
status: config?.initialStatus || ProviderStatus.NOT_READY,
initialize: jest.fn(() => {
return Promise.resolve('started');
Expand Down Expand Up @@ -40,13 +37,13 @@ describe('OpenFeature', () => {

describe('Requirement 1.1.2.1', () => {
it('should throw because the provider is not intended for the server', () => {
const provider = mockProvider({ runsOn: 'client'});
const provider = mockProvider({ runsOn: 'client' });
expect(() => OpenFeature.setProvider(provider)).toThrowError(
"Provider 'mock-events-success' is intended for use on the client."
);
});
it('should succeed because the provider is intended for the server', () => {
const provider = mockProvider({ runsOn: 'server'});
const provider = mockProvider({ runsOn: 'server' });
expect(() => OpenFeature.setProvider(provider)).not.toThrowError();
});
});
Expand All @@ -59,7 +56,7 @@ describe('OpenFeature', () => {
expect(provider.initialize).toHaveBeenCalled();
});

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

0 comments on commit d1f5049

Please sign in to comment.