learning_ai_common_plat/packages/feature-flag-client/src/client.test.ts

166 lines
4.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createFeatureFlagClient } from './client.js';
describe('createFeatureFlagClient', () => {
const baseConfig = {
baseUrl: 'http://localhost:4003/api',
productId: 'testapp',
platform: 'web',
};
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('should create a client with isEnabled returning false before init', () => {
const client = createFeatureFlagClient(baseConfig);
expect(client.isEnabled('any_flag')).toBe(false);
});
it('should fetch flags on init', async () => {
const mockFlags = { premium: true, beta_feature: false };
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ flags: mockFlags }),
})
);
const client = createFeatureFlagClient(baseConfig);
await client.init();
expect(client.isEnabled('premium')).toBe(true);
expect(client.isEnabled('beta_feature')).toBe(false);
expect(client.isEnabled('nonexistent')).toBe(false);
client.stop();
});
it('should return all flags via getAllFlags', async () => {
const mockFlags = { a: true, b: false };
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ flags: mockFlags }),
})
);
const client = createFeatureFlagClient(baseConfig);
await client.init();
expect(client.getAllFlags()).toEqual({ a: true, b: false });
client.stop();
});
it('should send correct headers', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ flags: {} }),
});
vi.stubGlobal('fetch', fetchMock);
const client = createFeatureFlagClient(baseConfig);
await client.init({ userId: 'user-123' });
expect(fetchMock).toHaveBeenCalledWith(
expect.stringContaining('/flags/poll?'),
expect.objectContaining({
headers: { 'x-product-id': 'testapp' },
})
);
const url = fetchMock.mock.calls[0][0] as string;
expect(url).toContain('platform=web');
expect(url).toContain('userId=user-123');
client.stop();
});
it('should keep existing flags on network error', async () => {
let callCount = 0;
vi.stubGlobal(
'fetch',
vi.fn().mockImplementation(() => {
callCount++;
if (callCount === 1) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ flags: { initial: true } }),
});
}
return Promise.reject(new Error('network'));
})
);
const client = createFeatureFlagClient(baseConfig);
await client.init();
expect(client.isEnabled('initial')).toBe(true);
await client.refresh();
expect(client.isEnabled('initial')).toBe(true);
client.stop();
});
it('should persist to storage when provided', async () => {
const store: Record<string, string> = {};
const storage = {
getItem: (k: string) => store[k] ?? null,
setItem: (k: string, v: string) => {
store[k] = v;
},
};
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ flags: { cached: true } }),
})
);
const client = createFeatureFlagClient({ ...baseConfig, storage });
await client.init();
expect(store['testapp-feature-flags']).toBeDefined();
expect(JSON.parse(store['testapp-feature-flags'])).toEqual({ cached: true });
client.stop();
});
it('should restore flags from storage on creation', () => {
const store: Record<string, string> = {
'testapp-feature-flags': JSON.stringify({ restored: true }),
};
const storage = {
getItem: (k: string) => store[k] ?? null,
setItem: (k: string, v: string) => {
store[k] = v;
},
};
const client = createFeatureFlagClient({ ...baseConfig, storage });
expect(client.isEnabled('restored')).toBe(true);
});
it('should stop polling on stop()', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ flags: {} }),
})
);
const client = createFeatureFlagClient({ ...baseConfig, pollIntervalMs: 1000 });
await client.init();
client.stop();
expect(client.isEnabled('anything')).toBe(false);
expect(client.getAllFlags()).toEqual({});
});
});