897 lines
31 KiB
TypeScript
897 lines
31 KiB
TypeScript
import type { EventDetails } from '@openfeature/core';
|
|
import { v4 as uuid } from 'uuid';
|
|
import type {
|
|
JsonValue,
|
|
Provider,
|
|
ProviderMetadata,
|
|
ResolutionDetails,
|
|
StaleEvent
|
|
} from '../src';
|
|
import {
|
|
NOOP_PROVIDER,
|
|
OpenFeature,
|
|
OpenFeatureEventEmitter,
|
|
ProviderEvents,
|
|
ProviderStatus
|
|
} from '../src';
|
|
|
|
const TIMEOUT = 1000;
|
|
const ERR_MESSAGE = 'fake err';
|
|
|
|
class MockProvider implements Provider {
|
|
readonly metadata: ProviderMetadata;
|
|
readonly events?: OpenFeatureEventEmitter;
|
|
readonly runsOn = 'client';
|
|
private hasInitialize: boolean;
|
|
private hasContextChanged: boolean;
|
|
private asyncContextChangedHandler: boolean;
|
|
private failOnInit: boolean;
|
|
private failOnContextChange: boolean;
|
|
private asyncDelay?: number;
|
|
private enableEvents: boolean;
|
|
onContextChange?: () => Promise<void> | void;
|
|
initialize?: () => Promise<void>;
|
|
|
|
constructor(options?: {
|
|
hasInitialize?: boolean;
|
|
asyncDelay?: number;
|
|
enableEvents?: boolean;
|
|
failOnInit?: boolean;
|
|
hasContextChanged?: boolean;
|
|
asyncContextChangedHandler?: boolean;
|
|
failOnContextChange?: boolean;
|
|
name?: string;
|
|
}) {
|
|
this.metadata = { name: options?.name ?? 'mock-provider' };
|
|
this.hasInitialize = options?.hasInitialize ?? true;
|
|
this.hasContextChanged = options?.hasContextChanged ?? true;
|
|
this.asyncDelay = options?.asyncDelay ?? 0;
|
|
this.enableEvents = options?.enableEvents ?? true;
|
|
this.failOnInit = options?.failOnInit ?? false;
|
|
this.failOnContextChange = options?.failOnContextChange ?? false;
|
|
this.asyncContextChangedHandler = options?.asyncContextChangedHandler ?? true;
|
|
if (this.hasContextChanged) {
|
|
this.onContextChange = this.changeHandler;
|
|
}
|
|
|
|
if (this.enableEvents) {
|
|
this.events = new OpenFeatureEventEmitter();
|
|
}
|
|
|
|
if (this.hasInitialize) {
|
|
this.initialize = jest.fn(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, this.asyncDelay));
|
|
if (this.failOnInit) {
|
|
throw new Error(ERR_MESSAGE);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
resolveBooleanEvaluation(): ResolutionDetails<boolean> {
|
|
throw new Error('Not implemented');
|
|
}
|
|
|
|
resolveNumberEvaluation(): ResolutionDetails<number> {
|
|
throw new Error('Not implemented');
|
|
}
|
|
|
|
resolveObjectEvaluation<T extends JsonValue>(): ResolutionDetails<T> {
|
|
throw new Error('Not implemented');
|
|
}
|
|
|
|
resolveStringEvaluation(): ResolutionDetails<string> {
|
|
throw new Error('Not implemented');
|
|
}
|
|
|
|
private changeHandler() {
|
|
if (this.asyncContextChangedHandler) {
|
|
return new Promise<void>((resolve, reject) =>
|
|
setTimeout(() => {
|
|
if (this.failOnContextChange) {
|
|
reject(new Error(ERR_MESSAGE));
|
|
} else {
|
|
resolve();
|
|
}
|
|
}, this.asyncDelay),
|
|
);
|
|
} else if (this.failOnContextChange) {
|
|
throw new Error(ERR_MESSAGE);
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('Events', () => {
|
|
// set timeouts short for this suite.
|
|
jest.setTimeout(TIMEOUT);
|
|
let domain = uuid();
|
|
|
|
afterEach(async () => {
|
|
await OpenFeature.clearProviders();
|
|
OpenFeature.clearHandlers();
|
|
jest.clearAllMocks();
|
|
domain = uuid();
|
|
// hacky, but it's helpful to clear the handlers between tests
|
|
/* 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();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
OpenFeature.setProvider(NOOP_PROVIDER);
|
|
});
|
|
|
|
describe('Requirement 5.1.1', () => {
|
|
describe('provider implements events', () => {
|
|
it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => {
|
|
const provider = new MockProvider();
|
|
const client = OpenFeature.getClient(domain);
|
|
client.addHandler(ProviderEvents.Ready, () => {
|
|
try {
|
|
expect(client.metadata.providerMetadata.name).toBe(provider.metadata.name);
|
|
expect(provider.initialize).toHaveBeenCalled();
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
}
|
|
});
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
|
|
it('It defines a mechanism for signalling `PROVIDER_ERROR`', (done) => {
|
|
//make sure an error event is fired when initialize promise reject
|
|
const provider = new MockProvider({ failOnInit: true });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Error, () => {
|
|
try {
|
|
expect(client.metadata.providerMetadata.name).toBe(provider.metadata.name);
|
|
expect(provider.initialize).toHaveBeenCalled();
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
}
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
});
|
|
|
|
describe('provider does not implement events', () => {
|
|
it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => {
|
|
const provider = new MockProvider({ enableEvents: false });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Ready, () => {
|
|
try {
|
|
expect(client.metadata.providerMetadata.name).toBe(provider.metadata.name);
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
}
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
|
|
it('It defines a mechanism for signalling `PROVIDER_ERROR`', (done) => {
|
|
const provider = new MockProvider({ enableEvents: false, failOnInit: true });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Error, () => {
|
|
try {
|
|
expect(client.metadata.providerMetadata.name).toBe(provider.metadata.name);
|
|
expect(provider.initialize).toHaveBeenCalled();
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
}
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.1.2', () => {
|
|
it('When a provider signals the occurrence of a particular event, the associated client and API event handlers run', (done) => {
|
|
const provider = new MockProvider();
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
Promise.all([
|
|
new Promise<void>((resolve) => {
|
|
client.addHandler(ProviderEvents.Error, () => {
|
|
resolve();
|
|
});
|
|
}),
|
|
new Promise<void>((resolve) => {
|
|
OpenFeature.addHandler(ProviderEvents.Error, () => {
|
|
resolve();
|
|
});
|
|
}),
|
|
]).then(() => {
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
provider.events?.emit(ProviderEvents.Error);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.1.3', () => {
|
|
it('When a provider signals the occurrence of a particular event, event handlers on clients which are not associated with that provider do not run', (done) => {
|
|
const provider = new MockProvider();
|
|
const client0 = OpenFeature.getClient(domain);
|
|
const client1 = OpenFeature.getClient(domain + '1');
|
|
|
|
const client1Handler = jest.fn();
|
|
const client0Handler = () => {
|
|
expect(client1Handler).not.toHaveBeenCalled();
|
|
done();
|
|
};
|
|
|
|
client0.addHandler(ProviderEvents.Ready, client0Handler);
|
|
client1.addHandler(ProviderEvents.Ready, client1Handler);
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
|
|
it('anonymous provider with anonymous client should run non-init events', (done) => {
|
|
const defaultProvider = new MockProvider({
|
|
failOnInit: false,
|
|
name: 'default',
|
|
});
|
|
|
|
// get a anon client
|
|
const anonClient = OpenFeature.getClient();
|
|
anonClient.addHandler(ProviderEvents.ConfigurationChanged, () => {
|
|
done();
|
|
});
|
|
|
|
// set the default provider
|
|
OpenFeature.setProvider(defaultProvider);
|
|
|
|
// fire events
|
|
defaultProvider.events?.emit(ProviderEvents.ConfigurationChanged);
|
|
});
|
|
|
|
it('anonymous provider with anonymous client should run init events', (done) => {
|
|
const defaultProvider = new MockProvider({
|
|
failOnInit: false,
|
|
name: 'default',
|
|
});
|
|
|
|
// get a anon client
|
|
const anonClient = OpenFeature.getClient();
|
|
anonClient.addHandler(ProviderEvents.Ready, () => {
|
|
done();
|
|
});
|
|
|
|
// set the default provider
|
|
OpenFeature.setProvider(defaultProvider);
|
|
});
|
|
|
|
it('anonymous provider with named client should run non-init events', (done) => {
|
|
const defaultProvider = new MockProvider({
|
|
failOnInit: false,
|
|
name: 'default',
|
|
});
|
|
const unboundName = 'some-new-unbound-name';
|
|
|
|
// get a client using the default because it has not other mapping
|
|
const unBoundClient = OpenFeature.getClient(unboundName);
|
|
unBoundClient.addHandler(ProviderEvents.ConfigurationChanged, () => {
|
|
done();
|
|
});
|
|
|
|
// set the default provider
|
|
OpenFeature.setProvider(defaultProvider);
|
|
|
|
// fire events
|
|
defaultProvider.events?.emit(ProviderEvents.ConfigurationChanged);
|
|
});
|
|
|
|
it('anonymous provider with named client should run init events', (done) => {
|
|
const defaultProvider = new MockProvider({
|
|
failOnInit: false,
|
|
name: 'default',
|
|
});
|
|
const unboundName = 'some-other-unbound-name';
|
|
|
|
// get a client using the default because it has not other mapping
|
|
const unBoundClient = OpenFeature.getClient(unboundName);
|
|
unBoundClient.addHandler(ProviderEvents.Ready, () => {
|
|
done();
|
|
});
|
|
|
|
// set the default provider
|
|
OpenFeature.setProvider(defaultProvider);
|
|
});
|
|
|
|
it('un-bound client event handlers still run after new provider set', (done) => {
|
|
const defaultProvider = new MockProvider({ name: 'default' });
|
|
const namedProvider = new MockProvider();
|
|
const unboundName = 'unboundName';
|
|
const boundName = 'boundName';
|
|
|
|
// set the default provider
|
|
OpenFeature.setProvider(defaultProvider);
|
|
|
|
// get a client using the default because it has not other mapping
|
|
const unBoundClient = OpenFeature.getClient(unboundName);
|
|
unBoundClient.addHandler(ProviderEvents.ConfigurationChanged, () => {
|
|
done();
|
|
});
|
|
|
|
// get a client and assign a provider to it
|
|
OpenFeature.setProvider(boundName, namedProvider);
|
|
OpenFeature.getClient(boundName);
|
|
|
|
// fire events
|
|
defaultProvider.events?.emit(ProviderEvents.ConfigurationChanged);
|
|
});
|
|
|
|
it('handler added while while provider initializing runs', (done) => {
|
|
const provider = new MockProvider({
|
|
name: 'race',
|
|
asyncDelay: TIMEOUT / 2,
|
|
});
|
|
|
|
// set the default provider
|
|
OpenFeature.setProvider(provider);
|
|
const client = OpenFeature.getClient();
|
|
|
|
// add a handler while the provider is starting
|
|
client.addHandler(ProviderEvents.Ready, () => {
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('PROVIDER_ERROR events populates the message field', (done) => {
|
|
const provider = new MockProvider({ failOnInit: true });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Error, (details) => {
|
|
expect(details?.message).toBeDefined();
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.2.1,', () => {
|
|
it('The client provides a function for associating handler functions with a particular provider event type', () => {
|
|
const client = OpenFeature.getClient(domain);
|
|
expect(client.addHandler).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.2.2,', () => {
|
|
it('The API provides a function for associating handler functions with a particular provider event type', () => {
|
|
expect(OpenFeature.addHandler).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.2.3,', () => {
|
|
it('The event details MUST contain the provider name associated with the event.', (done) => {
|
|
const providerName = '5.2.3';
|
|
const provider = new MockProvider({ name: providerName });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Ready, (details) => {
|
|
expect(details?.providerName).toEqual(providerName);
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
|
|
it('The event details contain the client name associated with the event in the client', (done) => {
|
|
const provider = new MockProvider();
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Ready, (details) => {
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.2.4', () => {
|
|
it('The handler function accepts a event details parameter.', (done) => {
|
|
const details: StaleEvent = { message: 'message' };
|
|
const provider = new MockProvider();
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Stale, (givenDetails) => {
|
|
expect(givenDetails?.message).toEqual(details.message);
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
provider.events?.emit(ProviderEvents.Stale, details);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.2.5', () => {
|
|
it('If a handler function terminates abnormally, other handler functions run', (done) => {
|
|
const provider = new MockProvider();
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
const handler0 = jest.fn(() => {
|
|
throw new Error('Error during initialization');
|
|
});
|
|
|
|
const handler1 = () => {
|
|
expect(handler0).toHaveBeenCalled();
|
|
done();
|
|
};
|
|
|
|
client.addHandler(ProviderEvents.Ready, handler0);
|
|
client.addHandler(ProviderEvents.Ready, handler1);
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.2.6 ', () => {
|
|
it('Event handlers MUST persist across `provider` changes.', (done) => {
|
|
const provider1 = new MockProvider({ name: 'provider-1' });
|
|
const provider2 = new MockProvider({ name: 'provider-2' });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
let counter = 0;
|
|
client.addHandler(ProviderEvents.Ready, () => {
|
|
if (client.metadata.providerMetadata.name === provider1.metadata.name) {
|
|
OpenFeature.setProvider(domain, provider2);
|
|
counter++;
|
|
} else {
|
|
expect(counter).toBeGreaterThan(0);
|
|
expect(client.metadata.providerMetadata.name).toBe(provider2.metadata.name);
|
|
if (counter == 1) {
|
|
done();
|
|
}
|
|
}
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider1);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.2.7 ', () => {
|
|
it('The API provides a function allowing the removal of event handlers', () => {
|
|
const handler = jest.fn();
|
|
const eventType = ProviderEvents.Stale;
|
|
|
|
OpenFeature.addHandler(eventType, handler);
|
|
expect(OpenFeature.getHandlers(eventType)).toHaveLength(1);
|
|
OpenFeature.removeHandler(eventType, handler);
|
|
expect(OpenFeature.getHandlers(eventType)).toHaveLength(0);
|
|
});
|
|
|
|
it('The event handler can be removed using an abort signal', () => {
|
|
const abortController = new AbortController();
|
|
const handler1 = jest.fn();
|
|
const handler2 = jest.fn();
|
|
const eventType = ProviderEvents.Stale;
|
|
|
|
OpenFeature.addHandler(eventType, handler1, { signal: abortController.signal });
|
|
OpenFeature.addHandler(eventType, handler2);
|
|
expect(OpenFeature.getHandlers(eventType)).toHaveLength(2);
|
|
|
|
abortController.abort();
|
|
expect(OpenFeature.getHandlers(eventType)).toHaveLength(1);
|
|
});
|
|
|
|
it('The API provides a function allowing the removal of event handlers from client', () => {
|
|
const client = OpenFeature.getClient(domain);
|
|
const handler = jest.fn();
|
|
const eventType = ProviderEvents.Stale;
|
|
|
|
client.addHandler(eventType, handler);
|
|
expect(client.getHandlers(eventType)).toHaveLength(1);
|
|
client.removeHandler(eventType, handler);
|
|
expect(client.getHandlers(eventType)).toHaveLength(0);
|
|
});
|
|
|
|
it('The event handler on the client can be removed using an abort signal', () => {
|
|
const abortController = new AbortController();
|
|
const client = OpenFeature.getClient(domain);
|
|
const handler1 = jest.fn();
|
|
const handler2 = jest.fn();
|
|
const eventType = ProviderEvents.Stale;
|
|
|
|
client.addHandler(eventType, handler1, { signal: abortController.signal });
|
|
client.addHandler(eventType, handler2);
|
|
expect(client.getHandlers(eventType)).toHaveLength(2);
|
|
|
|
abortController.abort();
|
|
expect(client.getHandlers(eventType)).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.3.1', () => {
|
|
it('If the provider `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run', (done) => {
|
|
const provider = new MockProvider();
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Ready, () => {
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.3.2', () => {
|
|
it('If the provider `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.', (done) => {
|
|
const provider = new MockProvider({ failOnInit: true });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
client.addHandler(ProviderEvents.Error, () => {
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
});
|
|
|
|
it('It defines a mechanism for signalling `PROVIDER_CONFIGURATION_CHANGED`', (done) => {
|
|
const provider = new MockProvider();
|
|
const client = OpenFeature.getClient(domain);
|
|
const changedFlag = 'fake-flag';
|
|
|
|
client.addHandler(ProviderEvents.ConfigurationChanged, (details) => {
|
|
expect(details?.flagsChanged?.length).toEqual(1);
|
|
expect(details?.flagsChanged).toEqual([changedFlag]);
|
|
done();
|
|
});
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
// emit a change event from the mock provider
|
|
provider.events?.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [changedFlag] });
|
|
});
|
|
|
|
it('It allows iteration over all event types', () => {
|
|
// just a typings test; it should be possible to iterate over a collection of ProviderEvents,
|
|
const client = OpenFeature.getClient(domain);
|
|
const providerEvents: ProviderEvents[] = [];
|
|
|
|
providerEvents.forEach((e) => {
|
|
client.addHandler(e, () => {});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.3.3', () => {
|
|
describe('API', () => {
|
|
describe('Handlers attached after the provider is already in the associated state, MUST run immediately.', () => {
|
|
it('Ready', (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false });
|
|
|
|
OpenFeature.setProviderAndWait(domain, provider).then(() => {
|
|
OpenFeature.addHandler(ProviderEvents.Ready, () => {
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('Error', (done) => {
|
|
const provider = new MockProvider({ failOnInit: true });
|
|
|
|
OpenFeature.setProviderAndWait(domain, provider).catch(() => {
|
|
OpenFeature.addHandler(ProviderEvents.Error, () => {
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('client', () => {
|
|
describe('Handlers attached after the provider is already in the associated state, MUST run immediately.', () => {
|
|
it('Ready', (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
OpenFeature.setProviderAndWait(domain, provider).then(() => {
|
|
client.addHandler(ProviderEvents.Ready, () => {
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('Error', (done) => {
|
|
const provider = new MockProvider({ failOnInit: true });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
OpenFeature.setProviderAndWait(domain, provider).catch(() => {
|
|
client.addHandler(ProviderEvents.Error, () => {
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.3.4.1', () => {
|
|
describe('API', () => {
|
|
describe('provider has context changed handler', () => {
|
|
it('Reconciling and ContextChanged are emitted', async () => {
|
|
const provider = new MockProvider({ hasInitialize: false, hasContextChanged: true });
|
|
|
|
const handler = jest.fn(() => {});
|
|
|
|
await OpenFeature.setProviderAndWait(domain, provider);
|
|
OpenFeature.addHandler(ProviderEvents.Reconciling, handler);
|
|
OpenFeature.addHandler(ProviderEvents.ContextChanged, handler);
|
|
await OpenFeature.setContext(domain, {});
|
|
|
|
expect(handler).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('Reconciling events are not emitted for synchronous onContextChange operations', async () => {
|
|
const provider = new MockProvider({
|
|
hasInitialize: false,
|
|
hasContextChanged: true,
|
|
asyncContextChangedHandler: false,
|
|
});
|
|
|
|
const reconcileHandler = jest.fn(() => {});
|
|
const changedEventHandler = jest.fn(() => {});
|
|
|
|
await OpenFeature.setProviderAndWait(domain, provider);
|
|
OpenFeature.addHandler(ProviderEvents.Reconciling, reconcileHandler);
|
|
OpenFeature.addHandler(ProviderEvents.ContextChanged, changedEventHandler);
|
|
await OpenFeature.setContext(domain, {});
|
|
|
|
expect(reconcileHandler).not.toHaveBeenCalled();
|
|
expect(changedEventHandler).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('provider has no context changed handler', () => {
|
|
it('only ContextChanged is emitted', async () => {
|
|
const provider = new MockProvider({ hasInitialize: false, hasContextChanged: false });
|
|
|
|
const handler = jest.fn(() => {});
|
|
|
|
await OpenFeature.setProviderAndWait(domain, provider);
|
|
OpenFeature.addHandler(ProviderEvents.Reconciling, handler); // this should not be called
|
|
OpenFeature.addHandler(ProviderEvents.ContextChanged, handler);
|
|
await OpenFeature.setContext(domain, {});
|
|
|
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('client', () => {
|
|
describe('provider has context changed handler', () => {
|
|
it('Stale and ContextChanged are emitted', async () => {
|
|
const provider = new MockProvider({ hasInitialize: false, hasContextChanged: true });
|
|
|
|
const handler = jest.fn(() => {});
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
await OpenFeature.setProviderAndWait(domain, provider);
|
|
client.addHandler(ProviderEvents.Reconciling, handler);
|
|
client.addHandler(ProviderEvents.ContextChanged, handler);
|
|
await OpenFeature.setContext(domain, {});
|
|
|
|
expect(handler).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('provider has no context changed handler', () => {
|
|
it('only ContextChanged is emitted', async () => {
|
|
const provider = new MockProvider({ hasInitialize: false, hasContextChanged: false });
|
|
|
|
const handler = jest.fn(() => {});
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
await OpenFeature.setProviderAndWait(domain, provider);
|
|
client.addHandler(ProviderEvents.Reconciling, handler); // this should not be called
|
|
client.addHandler(ProviderEvents.ContextChanged, handler);
|
|
await OpenFeature.setContext(domain, {});
|
|
|
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.3.4.2, 5.3.4.3', () => {
|
|
describe('API', () => {
|
|
describe('context set for same client', () => {
|
|
it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false });
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
OpenFeature.setContext(domain, {});
|
|
|
|
const handler = (details?: EventDetails) => {
|
|
try {
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
expect(details?.providerName).toEqual(provider.metadata.name);
|
|
done();
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
};
|
|
|
|
OpenFeature.addHandler(ProviderEvents.ContextChanged, handler);
|
|
});
|
|
|
|
it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false, failOnContextChange: true });
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
OpenFeature.setContext(domain, {});
|
|
|
|
const handler = (details?: EventDetails) => {
|
|
try {
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
expect(details?.providerName).toEqual(provider.metadata.name);
|
|
done();
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
};
|
|
|
|
OpenFeature.addHandler(ProviderEvents.Error, handler);
|
|
});
|
|
});
|
|
|
|
describe('context set for different client', () => {
|
|
it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false });
|
|
let runCount = 0;
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
|
|
// expect 2 runs, since 2 providers are impacted by this context change (global)
|
|
const handler = (details?: EventDetails) => {
|
|
try {
|
|
runCount++;
|
|
// one run should be global
|
|
if (details?.domain === undefined) {
|
|
expect(details?.providerName).toEqual(OpenFeature.getProviderMetadata().name);
|
|
} else if (details?.domain === domain) {
|
|
// one run should be for client
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
expect(details?.providerName).toEqual(provider.metadata.name);
|
|
}
|
|
if (runCount == 2) {
|
|
done();
|
|
}
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
};
|
|
|
|
OpenFeature.addHandler(ProviderEvents.ContextChanged, handler);
|
|
OpenFeature.setContext({});
|
|
});
|
|
|
|
it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false, failOnContextChange: true });
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
|
|
const handler = (details?: EventDetails) => {
|
|
try {
|
|
// expect only one error run, because only one provider throws
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
expect(details?.providerName).toEqual(provider.metadata.name);
|
|
expect(details?.message).toBeTruthy();
|
|
done();
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
};
|
|
|
|
OpenFeature.addHandler(ProviderEvents.Error, handler);
|
|
OpenFeature.setContext({});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('client', () => {
|
|
it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
|
|
const handler = (details?: EventDetails) => {
|
|
try {
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
expect(details?.providerName).toEqual(provider.metadata.name);
|
|
done();
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
};
|
|
|
|
client.addHandler(ProviderEvents.ContextChanged, handler);
|
|
OpenFeature.setContext(domain, {});
|
|
});
|
|
|
|
it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => {
|
|
const provider = new MockProvider({ hasInitialize: false, failOnContextChange: true });
|
|
const client = OpenFeature.getClient(domain);
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
|
|
const handler = (details?: EventDetails) => {
|
|
try {
|
|
expect(details?.clientName).toEqual(domain);
|
|
expect(details?.domain).toEqual(domain);
|
|
expect(details?.providerName).toEqual(provider.metadata.name);
|
|
expect(details?.message).toBeTruthy();
|
|
done();
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
};
|
|
|
|
client.addHandler(ProviderEvents.Error, handler);
|
|
OpenFeature.setContext(domain, {});
|
|
});
|
|
});
|
|
|
|
describe('reentrant invocations', () => {
|
|
it('only fire one terminal event', async () => {
|
|
// provider context change will take 100ms
|
|
const provider = new MockProvider({ hasInitialize: false, asyncDelay: 100 });
|
|
const client = OpenFeature.getClient(domain);
|
|
let runs = 0;
|
|
|
|
OpenFeature.setProvider(domain, provider);
|
|
|
|
const handler = () => {
|
|
runs++;
|
|
};
|
|
|
|
client.addHandler(ProviderEvents.ContextChanged, handler);
|
|
|
|
// update context change twice
|
|
await Promise.all([OpenFeature.setContext(domain, {}), OpenFeature.setContext(domain, {})]);
|
|
|
|
// should only have run once
|
|
expect(runs).toEqual(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Requirement 5.3.5', () => {
|
|
it('provider events update status', async () => {
|
|
// provider context change will take 100ms
|
|
const provider = new MockProvider({ hasInitialize: false });
|
|
OpenFeature.setProviderAndWait(domain, provider);
|
|
const client = OpenFeature.getClient(domain);
|
|
provider.events?.emit(ProviderEvents.Stale);
|
|
expect(client.providerStatus).toEqual(ProviderStatus.STALE);
|
|
|
|
provider.events?.emit(ProviderEvents.Ready);
|
|
expect(client.providerStatus).toEqual(ProviderStatus.READY);
|
|
|
|
provider.events?.emit(ProviderEvents.Error);
|
|
expect(client.providerStatus).toEqual(ProviderStatus.ERROR);
|
|
});
|
|
});
|
|
});
|