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();
 | 
						|
  });
 | 
						|
});
 |