Skip to content

Commit fc458c5

Browse files
committed
test: add unit tests for FlagManager debug override functionality
1 parent db57d3f commit fc458c5

File tree

7 files changed

+259
-24
lines changed

7 files changed

+259
-24
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { Context, Crypto, Hasher, LDLogger, Platform, Storage } from '@launchdarkly/js-sdk-common';
2+
3+
import DefaultFlagManager from '../../src/flag-manager/FlagManager';
4+
import { FlagsChangeCallback } from '../../src/flag-manager/FlagUpdater';
5+
import { ItemDescriptor } from '../../src/flag-manager/ItemDescriptor';
6+
import { Flag } from '../../src/types';
7+
8+
const TEST_SDK_KEY = 'test-sdk-key';
9+
const TEST_MAX_CACHED_CONTEXTS = 5;
10+
11+
function makeMockPlatform(storage: Storage, crypto: Crypto): Platform {
12+
return {
13+
storage,
14+
crypto,
15+
info: {
16+
platformData: jest.fn(),
17+
sdkData: jest.fn(),
18+
},
19+
requests: {
20+
fetch: jest.fn(),
21+
createEventSource: jest.fn(),
22+
getEventSourceCapabilities: jest.fn(),
23+
},
24+
};
25+
}
26+
27+
function makeMemoryStorage(): Storage {
28+
const data = new Map<string, string>();
29+
return {
30+
get: async (key: string) => {
31+
const value = data.get(key);
32+
return value !== undefined ? value : null;
33+
},
34+
set: async (key: string, value: string) => {
35+
data.set(key, value);
36+
},
37+
clear: async (key: string) => {
38+
data.delete(key);
39+
},
40+
};
41+
}
42+
43+
function makeMockCrypto() {
44+
let counter = 0;
45+
let lastInput = '';
46+
const hasher: Hasher = {
47+
update: jest.fn((input) => {
48+
lastInput = input;
49+
return hasher;
50+
}),
51+
digest: jest.fn(() => `${lastInput}Hashed`),
52+
};
53+
54+
return {
55+
createHash: jest.fn(() => hasher),
56+
createHmac: jest.fn(),
57+
randomUUID: jest.fn(() => {
58+
counter += 1;
59+
return `${counter}`;
60+
}),
61+
};
62+
}
63+
64+
function makeMockLogger(): LDLogger {
65+
return {
66+
error: jest.fn(),
67+
warn: jest.fn(),
68+
info: jest.fn(),
69+
debug: jest.fn(),
70+
};
71+
}
72+
73+
function makeMockFlag(version: number = 1, value: any = 'test-value'): Flag {
74+
return {
75+
version,
76+
flagVersion: version,
77+
value,
78+
variation: 0,
79+
trackEvents: false,
80+
};
81+
}
82+
83+
function makeMockItemDescriptor(version: number = 1, value: any = 'test-value'): ItemDescriptor {
84+
return {
85+
version,
86+
flag: makeMockFlag(version, value),
87+
};
88+
}
89+
90+
describe('FlagManager override tests', () => {
91+
let flagManager: DefaultFlagManager;
92+
let mockPlatform: Platform;
93+
let mockLogger: LDLogger;
94+
95+
beforeEach(() => {
96+
mockLogger = makeMockLogger();
97+
mockPlatform = makeMockPlatform(makeMemoryStorage(), makeMockCrypto());
98+
flagManager = new DefaultFlagManager(
99+
mockPlatform,
100+
TEST_SDK_KEY,
101+
TEST_MAX_CACHED_CONTEXTS,
102+
mockLogger,
103+
);
104+
});
105+
106+
it('setOverride takes precedence over flag store value', async () => {
107+
const context = Context.fromLDContext({ kind: 'user', key: 'user-key' });
108+
const flags = {
109+
'test-flag': makeMockItemDescriptor(1, 'store-value'),
110+
};
111+
112+
await flagManager.init(context, flags);
113+
expect(flagManager.get('test-flag')?.flag.value).toBe('store-value');
114+
115+
const debugOverride = flagManager.getDebugOverride();
116+
debugOverride?.setOverride('test-flag', 'override-value');
117+
118+
expect(flagManager.get('test-flag')?.flag.value).toBe('override-value');
119+
});
120+
121+
it('setOverride triggers flag change callback', () => {
122+
const mockCallback: FlagsChangeCallback = jest.fn();
123+
flagManager.on(mockCallback);
124+
125+
const debugOverride = flagManager.getDebugOverride();
126+
debugOverride?.setOverride('test-flag', 'override-value');
127+
128+
expect(mockCallback).toHaveBeenCalledTimes(1);
129+
expect(mockCallback).toHaveBeenCalledWith(null, ['test-flag'], 'override');
130+
});
131+
132+
it('removeOverride does nothing when override does not exist', () => {
133+
const debugOverride = flagManager.getDebugOverride();
134+
expect(() => {
135+
debugOverride?.removeOverride('non-existent-flag');
136+
}).not.toThrow();
137+
});
138+
139+
it('removeOverride reverts to flag store value when override is removed', async () => {
140+
const context = Context.fromLDContext({ kind: 'user', key: 'user-key' });
141+
const flags = {
142+
'test-flag': makeMockItemDescriptor(1, 'store-value'),
143+
};
144+
145+
await flagManager.init(context, flags);
146+
const debugOverride = flagManager.getDebugOverride();
147+
debugOverride?.setOverride('test-flag', 'override-value');
148+
expect(flagManager.get('test-flag')?.flag.value).toBe('override-value');
149+
150+
debugOverride?.removeOverride('test-flag');
151+
expect(flagManager.get('test-flag')?.flag.value).toBe('store-value');
152+
});
153+
154+
it('removeOverride triggers flag change callback', () => {
155+
const mockCallback: FlagsChangeCallback = jest.fn();
156+
flagManager.on(mockCallback);
157+
158+
const debugOverride = flagManager.getDebugOverride();
159+
debugOverride?.setOverride('test-flag', 'override-value');
160+
debugOverride?.removeOverride('test-flag');
161+
162+
expect(mockCallback).toHaveBeenCalledTimes(2);
163+
expect(mockCallback).toHaveBeenNthCalledWith(1, null, ['test-flag'], 'override');
164+
expect(mockCallback).toHaveBeenNthCalledWith(2, null, ['test-flag'], 'override');
165+
});
166+
167+
it('clearAllOverrides removes all overrides', () => {
168+
const debugOverride = flagManager.getDebugOverride();
169+
debugOverride?.setOverride('flag1', 'value1');
170+
debugOverride?.setOverride('flag2', 'value2');
171+
debugOverride?.setOverride('flag3', 'value3');
172+
173+
expect(Object.keys(flagManager.getAllOverrides())).toHaveLength(3);
174+
175+
debugOverride?.clearAllOverrides();
176+
expect(Object.keys(flagManager.getAllOverrides())).toHaveLength(0);
177+
});
178+
179+
it('clearAllOverrides triggers flag change callback for all flags', () => {
180+
const mockCallback: FlagsChangeCallback = jest.fn();
181+
flagManager.on(mockCallback);
182+
183+
const debugOverride = flagManager.getDebugOverride();
184+
debugOverride?.setOverride('flag1', 'value1');
185+
debugOverride?.setOverride('flag2', 'value2');
186+
(mockCallback as jest.Mock).mockClear();
187+
188+
debugOverride?.clearAllOverrides();
189+
expect(mockCallback).toHaveBeenCalledTimes(1);
190+
expect(mockCallback).toHaveBeenCalledWith(null, ['flag1', 'flag2'], 'override');
191+
});
192+
193+
it('getAllOverrides returns all overrides as ItemDescriptors', () => {
194+
const debugOverride = flagManager.getDebugOverride();
195+
debugOverride?.setOverride('flag1', 'value1');
196+
debugOverride?.setOverride('flag2', 42);
197+
debugOverride?.setOverride('flag3', true);
198+
199+
const overrides = debugOverride?.getAllOverrides();
200+
expect(overrides).toHaveProperty('flag1');
201+
expect(overrides).toHaveProperty('flag2');
202+
expect(overrides).toHaveProperty('flag3');
203+
expect(overrides?.flag1.flag.value).toBe('value1');
204+
expect(overrides?.flag2.flag.value).toBe(42);
205+
expect(overrides?.flag3.flag.value).toBe(true);
206+
expect(overrides?.flag1.version).toBe(0);
207+
expect(overrides?.flag2.version).toBe(0);
208+
expect(overrides?.flag3.version).toBe(0);
209+
});
210+
211+
it('getAll merges overrides with flag store values', async () => {
212+
const context = Context.fromLDContext({ kind: 'user', key: 'user-key' });
213+
const flags = {
214+
'store-flag': makeMockItemDescriptor(1, 'store-value'),
215+
'shared-flag': makeMockItemDescriptor(1, 'store-value'),
216+
};
217+
218+
await flagManager.init(context, flags);
219+
const debugOverride = flagManager.getDebugOverride();
220+
debugOverride?.setOverride('shared-flag', 'override-value');
221+
debugOverride?.setOverride('override-only-flag', 'override-value');
222+
223+
const allFlags = flagManager.getAll();
224+
expect(allFlags).toHaveProperty('store-flag');
225+
expect(allFlags).toHaveProperty('shared-flag');
226+
expect(allFlags).toHaveProperty('override-only-flag');
227+
expect(allFlags['store-flag'].flag.value).toBe('store-value');
228+
expect(allFlags['shared-flag'].flag.value).toBe('override-value');
229+
expect(allFlags['override-only-flag'].flag.value).toBe('override-value');
230+
});
231+
});

packages/shared/sdk-client/src/LDClientImpl.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
} from './evaluation/evaluationDetail';
4646
import createEventProcessor from './events/createEventProcessor';
4747
import EventFactory from './events/EventFactory';
48-
import DefaultFlagManager, { LDDebugOverride, FlagManager } from './flag-manager/FlagManager';
48+
import DefaultFlagManager, { FlagManager, LDDebugOverride } from './flag-manager/FlagManager';
4949
import { FlagChangeType } from './flag-manager/FlagUpdater';
5050
import { ItemDescriptor } from './flag-manager/ItemDescriptor';
5151
import HookRunner from './HookRunner';
@@ -609,10 +609,10 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
609609

610610
protected getDebugOverrides(): LDDebugOverride | null {
611611
if (this._flagManager.getDebugOverride) {
612-
return this._flagManager.getDebugOverride()
612+
return this._flagManager.getDebugOverride();
613613
}
614614

615-
return null
615+
return null;
616616
}
617617

618618
private _handleInspectionChanged(flagKeys: Array<string>, type: FlagChangeType) {
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { LDPluginBase } from '@launchdarkly/js-sdk-common';
2+
23
import { LDDebugOverride } from '../flag-manager/FlagManager';
34

45
export interface LDPlugin<TClient, THook> extends LDPluginBase<TClient, THook> {
5-
/**
6+
/**
67
* An optional function called if the plugin wants to register debug capabilities.
78
* This method allows plugins to receive a debug override interface for
89
* temporarily overriding flag values during development and testing.
@@ -12,5 +13,5 @@ export interface LDPlugin<TClient, THook> extends LDPluginBase<TClient, THook> {
1213
*
1314
* @param debugOverride The debug override interface instance
1415
*/
15-
registerDebug?(debugOverride: LDDebugOverride): void;
16+
registerDebug?(debugOverride: LDDebugOverride): void;
1617
}

packages/shared/sdk-client/src/api/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ export { ConnectionMode };
99
export * from './LDIdentifyOptions';
1010
export * from './LDInspection';
1111
export * from './LDIdentifyResult';
12-
export * from './LDPlugin';
12+
export * from './LDPlugin';

packages/shared/sdk-client/src/flag-manager/FlagManager.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export interface FlagManager {
7676
*
7777
* @experimental This function is experimental and intended for use by LaunchDarkly tools at this time.
7878
*/
79-
getDebugOverride?(): LDDebugOverride
79+
getDebugOverride?(): LDDebugOverride;
8080
}
8181

8282
/**
@@ -182,14 +182,16 @@ export default class DefaultFlagManager implements FlagManager {
182182
if (this._overrides) {
183183
return {
184184
...this._flagStore.getAll(),
185-
...Object.entries(this._overrides).reduce((acc: {[key: string]: ItemDescriptor}, [key, value]) => {
186-
acc[key] = this._convertValueToOverrideDescripter(value);
187-
return acc
188-
}, {})
189-
}
190-
} else {
191-
return this._flagStore.getAll();
185+
...Object.entries(this._overrides).reduce(
186+
(acc: { [key: string]: ItemDescriptor }, [key, value]) => {
187+
acc[key] = this._convertValueToOverrideDescripter(value);
188+
return acc;
189+
},
190+
{},
191+
),
192+
};
192193
}
194+
return this._flagStore.getAll();
193195
}
194196

195197
presetFlags(newFlags: { [key: string]: ItemDescriptor }): void {
@@ -225,10 +227,10 @@ export default class DefaultFlagManager implements FlagManager {
225227
private _convertValueToOverrideDescripter(value: LDFlagValue): ItemDescriptor {
226228
return {
227229
flag: {
228-
value: value,
229-
version: 0
230+
value,
231+
version: 0,
230232
},
231-
version: 0
233+
version: 0,
232234
};
233235
}
234236

packages/shared/sdk-client/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export type {
4141

4242
export type { DataManager, DataManagerFactory, ConnectionParams } from './DataManager';
4343
export type { FlagManager, LDDebugOverride } from './flag-manager/FlagManager';
44-
export {safeRegisterDebugOverridePlugins} from './plugins/safeRegisterDebugOverridePlugins';
44+
export { safeRegisterDebugOverridePlugins } from './plugins/safeRegisterDebugOverridePlugins';
4545
export type { Configuration } from './configuration/Configuration';
4646

4747
export type { LDEmitter };
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { internal, LDLogger } from "@launchdarkly/js-sdk-common";
2-
import { LDPlugin } from "../api";
3-
import { LDDebugOverride } from "../flag-manager/FlagManager";
1+
import { internal, LDLogger } from '@launchdarkly/js-sdk-common';
2+
3+
import { LDPlugin } from '../api';
4+
import { LDDebugOverride } from '../flag-manager/FlagManager';
45

56
/**
67
* Safe register debug override plugins.
@@ -12,13 +13,13 @@ import { LDDebugOverride } from "../flag-manager/FlagManager";
1213
export function safeRegisterDebugOverridePlugins<TClient, THook>(
1314
logger: LDLogger,
1415
debugOverride: LDDebugOverride,
15-
plugins: LDPlugin<TClient, THook>[]
16+
plugins: LDPlugin<TClient, THook>[],
1617
): void {
17-
plugins.forEach(plugin => {
18+
plugins.forEach((plugin) => {
1819
try {
1920
plugin.registerDebug?.(debugOverride);
2021
} catch (error) {
2122
logger.error(`Exception thrown registering plugin ${internal.safeGetName(logger, plugin)}.`);
2223
}
2324
});
24-
};
25+
}

0 commit comments

Comments
 (0)