import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { FeatureFlagService, MockFeatureFlagService, mockFeatureFlags } from './FeatureFlagService'; import * as apiBaseUrl from '../config/apiBaseUrl'; 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 () => { vi.spyOn(apiBaseUrl, 'getWebsiteApiBaseUrl').mockReturnValue('http://localhost:3001'); 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); }); });