Files
gridpilot.gg/apps/website/lib/feature/FeatureFlagService.test.ts
2026-01-07 22:05:53 +01:00

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