284 lines
9.6 KiB
TypeScript
284 lines
9.6 KiB
TypeScript
/*
|
|
* Copyright 2025 coze-dev Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { type Mock } from 'vitest';
|
|
|
|
import { featureFlagStorage } from '../src/utils/storage';
|
|
import {
|
|
readFgPromiseFromContext,
|
|
readFgValuesFromContext,
|
|
} from '../src/utils/read-from-context';
|
|
import { readFromCache, saveToCache } from '../src/utils/persist-cache';
|
|
|
|
const fetchFeatureGating = vi.fn();
|
|
|
|
const $wait = ms => new Promise(r => setTimeout(r, ms));
|
|
vi.mock('../src/utils/wait', () => ({
|
|
wait: vi.fn().mockImplementation($wait),
|
|
ONE_SEC: 1000,
|
|
nextTick: vi.fn().mockImplementation(async () => {
|
|
await $wait(10);
|
|
}),
|
|
}));
|
|
|
|
vi.mock('../src/utils/persist-cache', () => ({
|
|
readFromCache: vi.fn(),
|
|
saveToCache: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../src/utils/read-from-context', () => ({
|
|
readFgPromiseFromContext: vi.fn(),
|
|
readFgValuesFromContext: vi.fn().mockReturnValue(undefined),
|
|
}));
|
|
|
|
vi.mock('../src/utils/storage', () => ({
|
|
featureFlagStorage: {
|
|
setFlags: vi.fn(),
|
|
getFlags: vi.fn().mockReturnValue({}),
|
|
},
|
|
}));
|
|
|
|
describe('pullFeatureFlags', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.resetModules();
|
|
});
|
|
|
|
// Runs successfully with default context
|
|
it('should access values from global static value', async () => {
|
|
(readFgValuesFromContext as Mock).mockReturnValue({ foo: true });
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await pullFeatureFlags({ fetchFeatureGating });
|
|
|
|
expect(readFgValuesFromContext).toBeCalled();
|
|
expect(readFgPromiseFromContext).not.toBeCalled();
|
|
expect(readFromCache).not.toBeCalled();
|
|
expect(fetchFeatureGating).not.toBeCalled();
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
|
expect(saveToCache).toBeCalledWith({ foo: true });
|
|
});
|
|
|
|
// Runs successfully with default context
|
|
it('should access values from global context', async () => {
|
|
(readFgValuesFromContext as Mock).mockReturnValue(undefined);
|
|
readFgPromiseFromContext.mockResolvedValue({ foo: true });
|
|
readFromCache.mockResolvedValue(undefined);
|
|
fetchFeatureGating.mockResolvedValue(undefined);
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await pullFeatureFlags({ fetchFeatureGating });
|
|
|
|
expect(readFgPromiseFromContext).toBeCalled();
|
|
expect(readFromCache).toBeCalled();
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
|
expect(saveToCache).toBeCalledWith({ foo: true });
|
|
});
|
|
|
|
it('should access values from localstorage', async () => {
|
|
readFgPromiseFromContext.mockResolvedValueOnce(undefined);
|
|
readFromCache.mockResolvedValue({ foo: true });
|
|
fetchFeatureGating.mockResolvedValue(undefined);
|
|
readFgPromiseFromContext.mockResolvedValueOnce({ foo: false });
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
vi.useFakeTimers();
|
|
const p = pullFeatureFlags({ pollingInterval: 1000, fetchFeatureGating });
|
|
vi.runAllTimersAsync();
|
|
await p;
|
|
vi.useRealTimers();
|
|
|
|
expect(readFgPromiseFromContext).toBeCalled();
|
|
expect(readFromCache).toBeCalled();
|
|
expect(fetchFeatureGating).toBeCalled();
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
|
});
|
|
|
|
it('should access values from api', async () => {
|
|
readFgPromiseFromContext.mockResolvedValue(undefined);
|
|
readFromCache.mockResolvedValue(undefined);
|
|
fetchFeatureGating.mockResolvedValue({ foo: true });
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await pullFeatureFlags({ fetchFeatureGating });
|
|
|
|
expect(readFgPromiseFromContext).toBeCalled();
|
|
expect(readFromCache).toBeCalled();
|
|
expect(fetchFeatureGating).toBeCalled();
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
|
expect(saveToCache).toBeCalledWith({ foo: true });
|
|
});
|
|
|
|
it('should access values from global context firstly', async () => {
|
|
// When getting the value from both localStorage & global context, the context value is preferred
|
|
readFromCache.mockImplementation(async () => {
|
|
await $wait(100);
|
|
return { foo: true };
|
|
});
|
|
readFgPromiseFromContext.mockResolvedValue({ foo: false });
|
|
fetchFeatureGating.mockResolvedValue(undefined);
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await pullFeatureFlags({ fetchFeatureGating });
|
|
|
|
expect(readFgPromiseFromContext).toBeCalled();
|
|
expect(readFromCache).toBeCalled();
|
|
expect(fetchFeatureGating).toBeCalled();
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: false });
|
|
});
|
|
|
|
it('should fallback to default value', async () => {
|
|
readFromCache.mockResolvedValue(undefined);
|
|
readFgPromiseFromContext.mockResolvedValue(undefined);
|
|
fetchFeatureGating.mockResolvedValueOnce(undefined);
|
|
|
|
fetchFeatureGating.mockResolvedValueOnce({ foo: true });
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await pullFeatureFlags({
|
|
strict: false,
|
|
pollingInterval: 10,
|
|
timeout: 1000,
|
|
fetchFeatureGating,
|
|
});
|
|
|
|
expect(readFgPromiseFromContext).toBeCalledTimes(2);
|
|
expect(readFromCache).toBeCalledTimes(2);
|
|
expect(fetchFeatureGating).toBeCalledTimes(2);
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({});
|
|
});
|
|
|
|
it('should throw error with strict mode', async () => {
|
|
readFromCache.mockResolvedValue(undefined);
|
|
readFgPromiseFromContext.mockResolvedValue(undefined);
|
|
fetchFeatureGating.mockResolvedValue(undefined);
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await expect(
|
|
pullFeatureFlags({
|
|
strict: true,
|
|
pollingInterval: 10,
|
|
fetchFeatureGating,
|
|
}),
|
|
).rejects.toThrowError('Fetch Feature Flags timeout');
|
|
|
|
expect(readFgPromiseFromContext).toBeCalled();
|
|
expect(readFromCache).toBeCalled();
|
|
expect(fetchFeatureGating).toBeCalled();
|
|
expect(featureFlagStorage.setFlags).not.toBeCalled();
|
|
});
|
|
|
|
it('should throw error with strict mode once timeout', async () => {
|
|
const resolveAfterTimeout = async () => {
|
|
await $wait(2000);
|
|
return { foo: true };
|
|
};
|
|
readFromCache.mockImplementation(resolveAfterTimeout);
|
|
readFgPromiseFromContext.mockImplementation(resolveAfterTimeout);
|
|
fetchFeatureGating.mockImplementation(resolveAfterTimeout);
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await expect(
|
|
pullFeatureFlags({
|
|
strict: true,
|
|
timeout: 1000,
|
|
pollingInterval: 10,
|
|
fetchFeatureGating,
|
|
}),
|
|
).rejects.toThrowError('Fetch Feature Flags timeout');
|
|
|
|
expect(readFgPromiseFromContext).toBeCalled();
|
|
expect(readFromCache).toBeCalled();
|
|
expect(fetchFeatureGating).toBeCalled();
|
|
expect(featureFlagStorage.setFlags).not.toBeCalled();
|
|
});
|
|
|
|
it('should resolve value even if any step failure', async () => {
|
|
readFromCache.mockResolvedValue({ foo: true });
|
|
readFgPromiseFromContext.mockRejectedValue(new Error('jweofj'));
|
|
readFgValuesFromContext.mockReturnValue(undefined);
|
|
fetchFeatureGating.mockRejectedValueOnce(new Error('test'));
|
|
fetchFeatureGating.mockResolvedValue({ foo: false });
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
vi.useFakeTimers();
|
|
const p = pullFeatureFlags({
|
|
pollingInterval: 10,
|
|
timeout: 10,
|
|
fetchFeatureGating,
|
|
});
|
|
vi.runAllTimersAsync();
|
|
await p;
|
|
vi.useRealTimers();
|
|
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
|
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: false });
|
|
});
|
|
|
|
it('should fallback to default value & and retry even if all step failure', async () => {
|
|
readFromCache.mockRejectedValue(new Error('wfe'));
|
|
readFgPromiseFromContext.mockRejectedValue(new Error('jweofj'));
|
|
fetchFeatureGating.mockRejectedValueOnce(new Error('test'));
|
|
|
|
fetchFeatureGating.mockResolvedValue({ foo: true });
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await pullFeatureFlags({ pollingInterval: 10, fetchFeatureGating });
|
|
|
|
expect(fetchFeatureGating).toBeCalledTimes(2);
|
|
expect(featureFlagStorage.setFlags.mock.calls[0][0]).toEqual({});
|
|
expect(featureFlagStorage.setFlags.mock.calls[1][0]).toEqual({ foo: true });
|
|
});
|
|
|
|
it('should throw Error if all step break down', async () => {
|
|
(readFgValuesFromContext as Mock).mockImplementationOnce(() => {
|
|
throw new Error('test');
|
|
});
|
|
readFromCache.mockRejectedValue(new Error('wfe'));
|
|
readFgPromiseFromContext.mockRejectedValue(new Error('jweofj'));
|
|
fetchFeatureGating.mockRejectedValue(new Error('test'));
|
|
|
|
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
|
|
|
// Invoke function
|
|
await expect(
|
|
pullFeatureFlags({ strict: true, timeout: 1000, fetchFeatureGating }),
|
|
).rejects.toThrowError('Fetch Feature Flags timeout');
|
|
|
|
expect(featureFlagStorage.setFlags).not.toBeCalled();
|
|
});
|
|
});
|