Skip to content

Commit 5758a9b

Browse files
committed
tests
1 parent 0098fc2 commit 5758a9b

File tree

3 files changed

+204
-26
lines changed

3 files changed

+204
-26
lines changed

lib/project_config/polling_datafile_manager.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { getMockLogger } from '../tests/mock/mockLogger';
2323
import { DEFAULT_URL_TEMPLATE, MIN_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './config';
2424
import { resolvablePromise } from '../utils/promise/resolvablePromise';
2525
import { ServiceState } from '../service';
26+
import exp from 'constants';
2627

2728
const testCache = (): PersistentKeyValueCache => ({
2829
get(key: string): Promise<string | undefined> {
@@ -828,7 +829,22 @@ describe('PollingDatafileManager', () => {
828829
});
829830

830831
describe('stop', () => {
831-
it('rejects onRunning when stop is called if manager is new', async () => {
832+
it('rejects onRunning when stop is called if manager state is New', async () => {
833+
const repeater = getMockRepeater();
834+
const requestHandler = getMockRequestHandler();
835+
const manager = new PollingDatafileManager({
836+
repeater,
837+
requestHandler,
838+
sdkKey: 'keyThatExists',
839+
autoUpdate: true,
840+
});
841+
842+
expect(manager.getState()).toBe(ServiceState.New);
843+
manager.stop();
844+
await expect(manager.onRunning()).rejects.toThrow();
845+
});
846+
847+
it('rejects onRunning when stop is called if manager state is Starting', async () => {
832848
const repeater = getMockRepeater();
833849
const requestHandler = getMockRequestHandler();
834850
const manager = new PollingDatafileManager({
@@ -839,6 +855,7 @@ describe('PollingDatafileManager', () => {
839855
});
840856

841857
manager.start();
858+
expect(manager.getState()).toBe(ServiceState.Starting);
842859
manager.stop();
843860
await expect(manager.onRunning()).rejects.toThrow();
844861
});

lib/project_config/project_config_manager.spec.ts

Lines changed: 160 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import * as testData from '../tests/test_data';
2121
import { createProjectConfig } from './project_config';
2222
import { resolvablePromise } from '../utils/promise/resolvablePromise';
2323
import { getMockDatafileManager } from '../tests/mock/mockDatafileManager';
24-
import exp from 'constants';
24+
import { wait } from '../../tests/testUtils';
2525

2626
const cloneDeep = (x: any) => JSON.parse(JSON.stringify(x));
2727

@@ -30,54 +30,54 @@ describe('ProjectConfigManagerImpl', () => {
3030
const logger = getMockLogger();
3131
const manager = new ProjectConfigManagerImpl({ logger});
3232
manager.start();
33-
await expect(manager.onRunning()).rejects.toBeTruthy();
33+
await expect(manager.onRunning()).rejects.toThrow();
3434
expect(logger.error).toHaveBeenCalled();
3535
});
3636

3737
it('should set status to Failed if neither datafile nor a datafileManager is passed into the constructor', async () => {
3838
const logger = getMockLogger();
3939
const manager = new ProjectConfigManagerImpl({ logger});
4040
manager.start();
41-
await expect(manager.onRunning()).rejects.toBeTruthy();
41+
await expect(manager.onRunning()).rejects.toThrow();
4242
expect(manager.getState()).toBe(ServiceState.Failed);
4343
});
4444

45-
it('should reject onTerminated', async () => {
45+
it('should reject onTerminated if neither datafile nor a datafileManager is passed into the constructor', async () => {
4646
const logger = getMockLogger();
4747
const manager = new ProjectConfigManagerImpl({ logger});
4848
manager.start();
49-
await expect(manager.onTerminated()).rejects.toBeTruthy();
49+
await expect(manager.onTerminated()).rejects.toThrow();
5050
});
5151

5252
describe('when constructed with only a datafile', () => {
5353
it('should reject onRunning() and log error if the datafile is invalid', async () => {
5454
const logger = getMockLogger();
5555
const manager = new ProjectConfigManagerImpl({ logger, datafile: {}});
5656
manager.start();
57-
await expect(manager.onRunning()).rejects.toBeTruthy();
57+
await expect(manager.onRunning()).rejects.toThrow();
5858
expect(logger.error).toHaveBeenCalled();
5959
});
6060

6161
it('should set status to Failed if the datafile is invalid', async () => {
6262
const logger = getMockLogger();
6363
const manager = new ProjectConfigManagerImpl({ logger, datafile: {}});
6464
manager.start();
65-
await expect(manager.onRunning()).rejects.toBeTruthy();
65+
await expect(manager.onRunning()).rejects.toThrow();
6666
expect(manager.getState()).toBe(ServiceState.Failed);
6767
});
6868

6969
it('should reject onTerminated if the datafile is invalid', async () => {
7070
const logger = getMockLogger();
7171
const manager = new ProjectConfigManagerImpl({ logger});
7272
manager.start();
73-
await expect(manager.onTerminated()).rejects.toBeTruthy();
73+
await expect(manager.onTerminated()).rejects.toThrow();
7474
});
7575

7676
it('should fulfill onRunning() and set status to Running if the datafile is valid', async () => {
7777
const logger = getMockLogger();
7878
const manager = new ProjectConfigManagerImpl({ logger, datafile: testData.getTestProjectConfig()});
7979
manager.start();
80-
await expect(manager.onRunning()).resolves.toBeUndefined();
80+
await expect(manager.onRunning()).resolves.not.toThrow();
8181
expect(manager.getState()).toBe(ServiceState.Running);
8282
});
8383

@@ -123,7 +123,7 @@ describe('ProjectConfigManagerImpl', () => {
123123
manager.start();
124124

125125
expect(datafileManager.onRunning).toHaveBeenCalled();
126-
await expect(manager.onRunning()).resolves.toBeUndefined();
126+
await expect(manager.onRunning()).resolves.not.toThrow();
127127
});
128128

129129
it('should resolve onRunning() even if datafileManger.onRunning() rejects', async () => {
@@ -136,7 +136,7 @@ describe('ProjectConfigManagerImpl', () => {
136136
manager.start();
137137

138138
expect(datafileManager.onRunning).toHaveBeenCalled();
139-
await expect(manager.onRunning()).resolves.toBeUndefined();
139+
await expect(manager.onRunning()).resolves.not.toThrow();
140140
});
141141

142142
it('should call the onUpdate handler before datafileManger.onRunning() resolves', async () => {
@@ -149,7 +149,7 @@ describe('ProjectConfigManagerImpl', () => {
149149
const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager });
150150
manager.start();
151151
manager.onUpdate(listener);
152-
await expect(manager.onRunning()).resolves.toBeUndefined();
152+
await expect(manager.onRunning()).resolves.not.toThrow();
153153
expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig()));
154154
});
155155

@@ -180,7 +180,7 @@ describe('ProjectConfigManagerImpl', () => {
180180
const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager });
181181
manager.start();
182182
datafileManager.pushUpdate(testData.getTestProjectConfig());
183-
await expect(manager.onRunning()).resolves.toBeUndefined();
183+
await expect(manager.onRunning()).resolves.not.toThrow();
184184
expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig()));
185185
});
186186

@@ -192,10 +192,27 @@ describe('ProjectConfigManagerImpl', () => {
192192
manager.onUpdate(listener);
193193

194194
datafileManager.pushUpdate(testData.getTestProjectConfig());
195-
await expect(manager.onRunning()).resolves.toBeUndefined();
195+
await expect(manager.onRunning()).resolves.not.toThrow();
196196
expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig()));
197197
expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig()));
198198
});
199+
200+
it('should return undefined from getConfig() before onRunning() resolves', async () => {
201+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() });
202+
const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager });
203+
manager.start();
204+
expect(manager.getConfig()).toBeUndefined();
205+
});
206+
207+
it('should return the correct config from getConfig() after onRunning() resolves', async () => {
208+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() });
209+
const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager });
210+
manager.start();
211+
212+
datafileManager.pushUpdate(testData.getTestProjectConfig());
213+
await expect(manager.onRunning()).resolves.not.toThrow();
214+
expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig()));
215+
});
199216
});
200217
});
201218

@@ -215,10 +232,27 @@ describe('ProjectConfigManagerImpl', () => {
215232
manager.onUpdate(listener);
216233

217234
datafileManager.pushUpdate(testData.getTestProjectConfig());
218-
await expect(manager.onRunning()).resolves.toBeUndefined();
235+
await expect(manager.onRunning()).resolves.not.toThrow();
219236
expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig()));
220237
expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig()));
221238
});
239+
240+
it('should return undefined from getConfig() before onRunning() resolves', async () => {
241+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() });
242+
const manager = new ProjectConfigManagerImpl({ datafileManager });
243+
manager.start();
244+
expect(manager.getConfig()).toBeUndefined();
245+
});
246+
247+
it('should return the correct config from getConfig() after onRunning() resolves', async () => {
248+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() });
249+
const manager = new ProjectConfigManagerImpl({ datafileManager });
250+
manager.start();
251+
252+
datafileManager.pushUpdate(testData.getTestProjectConfig());
253+
await expect(manager.onRunning()).resolves.not.toThrow();
254+
expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig()));
255+
});
222256
});
223257

224258
it('should update the config and call onUpdate handlers when datafileManager onUpdate is fired with valid datafile', async () => {
@@ -347,6 +381,117 @@ describe('ProjectConfigManagerImpl', () => {
347381
expect(listener).toHaveBeenCalledWith(createProjectConfig(datafile));
348382
expect(manager.getConfig()).toEqual(createProjectConfig(datafile));
349383
});
384+
385+
describe('stop()', () => {
386+
it('should reject onRunning() if stop is called when the datafileManager state is New', async () => {
387+
const datafileManager = getMockDatafileManager({});
388+
const manager = new ProjectConfigManagerImpl({ datafileManager });
389+
390+
expect(manager.getState()).toBe(ServiceState.New);
391+
manager.stop();
392+
await expect(manager.onRunning()).rejects.toThrow();
393+
});
394+
395+
it('should reject onRunning() if stop is called when the datafileManager state is Starting', async () => {
396+
const datafileManager = getMockDatafileManager({});
397+
const manager = new ProjectConfigManagerImpl({ datafileManager });
398+
399+
manager.start();
400+
expect(manager.getState()).toBe(ServiceState.Starting);
401+
manager.stop();
402+
await expect(manager.onRunning()).rejects.toThrow();
403+
});
404+
405+
it('should call datafileManager.stop()', async () => {
406+
const datafileManager = getMockDatafileManager({});
407+
const spy = vi.spyOn(datafileManager, 'stop');
408+
const manager = new ProjectConfigManagerImpl({ datafileManager });
409+
manager.start();
410+
manager.stop();
411+
expect(spy).toHaveBeenCalled();
412+
});
413+
414+
it('should set status to Terminated immediately if no datafile manager is provided and resolve onTerminated', async () => {
415+
const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig() });
416+
manager.stop();
417+
expect(manager.getState()).toBe(ServiceState.Terminated);
418+
await expect(manager.onTerminated()).resolves.not.toThrow();
419+
});
420+
421+
it('should set status to Stopping while awaiting for datafileManager onTerminated', async () => {
422+
const datafileManagerTerminated = resolvablePromise<void>();
423+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise });
424+
const manager = new ProjectConfigManagerImpl({ datafileManager });
425+
manager.start();
426+
datafileManager.pushUpdate(testData.getTestProjectConfig());
427+
await manager.onRunning();
428+
manager.stop();
429+
430+
for (let i = 0; i < 100; i++) {
431+
expect(manager.getState()).toBe(ServiceState.Stopping);
432+
await wait(0);
433+
}
434+
});
435+
436+
it('should set status to Terminated and resolve onTerminated after datafileManager.onTerminated() resolves', async () => {
437+
const datafileManagerTerminated = resolvablePromise<void>();
438+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise });
439+
const manager = new ProjectConfigManagerImpl({ datafileManager });
440+
manager.start();
441+
datafileManager.pushUpdate(testData.getTestProjectConfig());
442+
await manager.onRunning();
443+
manager.stop();
444+
445+
for (let i = 0; i < 50; i++) {
446+
expect(manager.getState()).toBe(ServiceState.Stopping);
447+
await wait(0);
448+
}
449+
450+
datafileManagerTerminated.resolve();
451+
await expect(manager.onTerminated()).resolves.not.toThrow();
452+
expect(manager.getState()).toBe(ServiceState.Terminated);
453+
});
454+
455+
it('should set status to Failed and reject onTerminated after datafileManager.onTerminated() rejects', async () => {
456+
const datafileManagerTerminated = resolvablePromise<void>();
457+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise });
458+
const manager = new ProjectConfigManagerImpl({ datafileManager });
459+
manager.start();
460+
datafileManager.pushUpdate(testData.getTestProjectConfig());
461+
await manager.onRunning();
462+
manager.stop();
463+
464+
for (let i = 0; i < 50; i++) {
465+
expect(manager.getState()).toBe(ServiceState.Stopping);
466+
await wait(0);
467+
}
468+
469+
datafileManagerTerminated.reject();
470+
await expect(manager.onTerminated()).rejects.toThrow();
471+
expect(manager.getState()).toBe(ServiceState.Failed);
472+
});
473+
474+
it('should not call onUpdate handlers after stop is called', async () => {
475+
const datafileManagerTerminated = resolvablePromise<void>();
476+
const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise });
477+
const manager = new ProjectConfigManagerImpl({ datafileManager });
478+
manager.start();
479+
const listener = vi.fn();
480+
manager.onUpdate(listener);
481+
482+
datafileManager.pushUpdate(testData.getTestProjectConfig());
483+
await manager.onRunning();
484+
485+
expect(listener).toHaveBeenCalledTimes(1);
486+
manager.stop();
487+
datafileManager.pushUpdate(testData.getTestProjectConfigWithFeatures());
488+
489+
datafileManagerTerminated.resolve();
490+
await expect(manager.onTerminated()).resolves.not.toThrow();
491+
492+
expect(listener).toHaveBeenCalledTimes(1);
493+
});
494+
});
350495
});
351496

352497
// it('should call the error handler and fulfill onReady with an unsuccessful result if the datafile JSON is malformed', function() {

lib/project_config/project_config_manager.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf
131131
* the project config and optimizely config objects will not be updated, and the error is returned.
132132
*/
133133
private handleNewDatafile(newDatafile: string | object): void {
134+
if (this.isDone()) {
135+
return;
136+
}
137+
134138
try {
135139
const config = tryCreatingProjectConfig({
136140
datafile: newDatafile,
@@ -187,18 +191,30 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf
187191
* Stop the internal datafile manager and remove all update listeners
188192
*/
189193
stop(): void {
194+
if (this.isDone()) {
195+
return;
196+
}
197+
198+
if (this.isNew() || this.isStarting()) {
199+
// TOOD: replace message with imported constants
200+
this.startPromise.reject(new Error('Datafile manager stopped before it could be started'));
201+
}
202+
190203
this.state = ServiceState.Stopping;
191204
this.eventEmitter.removeAllListeners();
192-
193-
if (this.datafileManager) {
194-
this.datafileManager.stop();
195-
this.datafileManager.onTerminated().then(() => {
196-
this.state = ServiceState.Terminated;
197-
this.stopPromise.resolve();
198-
}).catch((err) => {
199-
this.state = ServiceState.Failed;
200-
this.stopPromise.reject(err);
201-
});
205+
if (!this.datafileManager) {
206+
this.state = ServiceState.Terminated;
207+
this.stopPromise.resolve();
208+
return;
202209
}
210+
211+
this.datafileManager.stop();
212+
this.datafileManager.onTerminated().then(() => {
213+
this.state = ServiceState.Terminated;
214+
this.stopPromise.resolve();
215+
}).catch((err) => {
216+
this.state = ServiceState.Failed;
217+
this.stopPromise.reject(err);
218+
});
203219
}
204220
}

0 commit comments

Comments
 (0)