192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { FeatureFlagService, MockFeatureFlagService, mockFeatureFlags } from './FeatureFlagService';
|
|
|
|
describe('FeatureFlagService', () => {
|
|
describe('fromAPI()', () => {
|
|
let originalBaseUrl: string | undefined;
|
|
let fetchMock: any;
|
|
|
|
beforeEach(() => {
|
|
originalBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
|
// Mock fetch globally
|
|
fetchMock = vi.fn();
|
|
global.fetch = fetchMock;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalBaseUrl !== undefined) {
|
|
process.env.NEXT_PUBLIC_API_BASE_URL = originalBaseUrl;
|
|
} else {
|
|
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
|
}
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should fetch from API and enable flags with value "enabled"', async () => {
|
|
process.env.NEXT_PUBLIC_API_BASE_URL = 'http://api.example.com';
|
|
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
features: {
|
|
driver_profiles: 'enabled',
|
|
team_profiles: 'enabled',
|
|
wallets: 'disabled',
|
|
sponsors: 'enabled',
|
|
team_feature: 'disabled',
|
|
alpha_features: 'enabled'
|
|
}
|
|
})
|
|
});
|
|
|
|
const service = await FeatureFlagService.fromAPI();
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://api.example.com/features',
|
|
{ next: { revalidate: 0 } }
|
|
);
|
|
expect(service.isEnabled('driver_profiles')).toBe(true);
|
|
expect(service.isEnabled('team_profiles')).toBe(true);
|
|
expect(service.isEnabled('sponsors')).toBe(true);
|
|
expect(service.isEnabled('alpha_features')).toBe(true);
|
|
expect(service.isEnabled('wallets')).toBe(false);
|
|
expect(service.isEnabled('team_feature')).toBe(false);
|
|
});
|
|
|
|
it('should use default localhost URL when NEXT_PUBLIC_API_BASE_URL is not set', async () => {
|
|
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
|
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
features: {
|
|
alpha_features: 'enabled'
|
|
}
|
|
})
|
|
});
|
|
|
|
await FeatureFlagService.fromAPI();
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:3001/features',
|
|
{ next: { revalidate: 0 } }
|
|
);
|
|
});
|
|
|
|
it('should return empty flags on HTTP error', async () => {
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
statusText: 'Internal Server Error'
|
|
});
|
|
|
|
const service = await FeatureFlagService.fromAPI();
|
|
|
|
expect(service.isEnabled('any_flag')).toBe(false);
|
|
expect(service.getEnabledFlags()).toEqual([]);
|
|
});
|
|
|
|
it('should return empty flags on network error', async () => {
|
|
fetchMock.mockRejectedValueOnce(new Error('Network error'));
|
|
|
|
const service = await FeatureFlagService.fromAPI();
|
|
|
|
expect(service.isEnabled('any_flag')).toBe(false);
|
|
expect(service.getEnabledFlags()).toEqual([]);
|
|
});
|
|
|
|
it('should handle empty features object', async () => {
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ features: {} })
|
|
});
|
|
|
|
const service = await FeatureFlagService.fromAPI();
|
|
|
|
expect(service.getEnabledFlags()).toEqual([]);
|
|
});
|
|
|
|
it('should handle malformed response', async () => {
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({})
|
|
});
|
|
|
|
const service = await FeatureFlagService.fromAPI();
|
|
|
|
expect(service.getEnabledFlags()).toEqual([]);
|
|
});
|
|
|
|
it('should ignore non-"enabled" values', async () => {
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
features: {
|
|
flag1: 'enabled',
|
|
flag2: 'disabled',
|
|
flag3: 'pending',
|
|
flag4: 'ENABLED', // case sensitive
|
|
flag5: ''
|
|
}
|
|
})
|
|
});
|
|
|
|
const service = await FeatureFlagService.fromAPI();
|
|
|
|
expect(service.isEnabled('flag1')).toBe(true);
|
|
expect(service.isEnabled('flag2')).toBe(false);
|
|
expect(service.isEnabled('flag3')).toBe(false);
|
|
expect(service.isEnabled('flag4')).toBe(false);
|
|
expect(service.isEnabled('flag5')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Constructor behavior', () => {
|
|
it('should use provided flags array', () => {
|
|
const service = new FeatureFlagService(['test_flag']);
|
|
expect(service.isEnabled('test_flag')).toBe(true);
|
|
expect(service.isEnabled('other_flag')).toBe(false);
|
|
});
|
|
|
|
it('should parse FEATURE_FLAGS environment variable', () => {
|
|
process.env.FEATURE_FLAGS = 'flag1, flag2, flag3';
|
|
const service = new FeatureFlagService();
|
|
|
|
expect(service.isEnabled('flag1')).toBe(true);
|
|
expect(service.isEnabled('flag2')).toBe(true);
|
|
expect(service.isEnabled('flag3')).toBe(true);
|
|
expect(service.isEnabled('flag4')).toBe(false);
|
|
|
|
delete process.env.FEATURE_FLAGS;
|
|
});
|
|
|
|
it('should handle empty FEATURE_FLAGS', () => {
|
|
process.env.FEATURE_FLAGS = '';
|
|
const service = new FeatureFlagService();
|
|
|
|
expect(service.isEnabled('any_flag')).toBe(false);
|
|
expect(service.getEnabledFlags()).toEqual([]);
|
|
|
|
delete process.env.FEATURE_FLAGS;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('MockFeatureFlagService', () => {
|
|
it('should work with provided flags', () => {
|
|
const service = new MockFeatureFlagService(['test_flag']);
|
|
expect(service.isEnabled('test_flag')).toBe(true);
|
|
expect(service.isEnabled('other_flag')).toBe(false);
|
|
});
|
|
|
|
it('should return empty array when no flags provided', () => {
|
|
const service = new MockFeatureFlagService();
|
|
expect(service.getEnabledFlags()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('mockFeatureFlags default instance', () => {
|
|
it('should have alpha_features enabled by default', () => {
|
|
expect(mockFeatureFlags.isEnabled('alpha_features')).toBe(true);
|
|
});
|
|
}); |