feature flags
This commit is contained in:
@@ -1,95 +1,143 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { FeatureFlagService, MockFeatureFlagService, mockFeatureFlags } from './FeatureFlagService';
|
||||
|
||||
describe('FeatureFlagService', () => {
|
||||
describe('fromEnv() with alpha mode integration', () => {
|
||||
let originalMode: string | undefined;
|
||||
let originalFlags: string | undefined;
|
||||
describe('fromAPI()', () => {
|
||||
let originalBaseUrl: string | undefined;
|
||||
let fetchMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
originalMode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
originalFlags = process.env.FEATURE_FLAGS;
|
||||
originalBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
// Mock fetch globally
|
||||
fetchMock = vi.fn();
|
||||
global.fetch = fetchMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalMode !== undefined) {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = originalMode;
|
||||
if (originalBaseUrl !== undefined) {
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL = originalBaseUrl;
|
||||
} else {
|
||||
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
}
|
||||
|
||||
if (originalFlags !== undefined) {
|
||||
process.env.FEATURE_FLAGS = originalFlags;
|
||||
} else {
|
||||
delete process.env.FEATURE_FLAGS;
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should enable all features when NEXT_PUBLIC_GRIDPILOT_MODE is alpha', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
|
||||
|
||||
const service = FeatureFlagService.fromEnv();
|
||||
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('wallets')).toBe(true);
|
||||
expect(service.isEnabled('sponsors')).toBe(true);
|
||||
expect(service.isEnabled('team_feature')).toBe(true);
|
||||
expect(service.isEnabled('alpha_features')).toBe(true);
|
||||
});
|
||||
|
||||
it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is pre-launch', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
|
||||
|
||||
const service = FeatureFlagService.fromEnv();
|
||||
|
||||
expect(service.isEnabled('driver_profiles')).toBe(false);
|
||||
expect(service.isEnabled('team_profiles')).toBe(false);
|
||||
expect(service.isEnabled('wallets')).toBe(false);
|
||||
expect(service.isEnabled('sponsors')).toBe(false);
|
||||
expect(service.isEnabled('team_feature')).toBe(false);
|
||||
expect(service.isEnabled('alpha_features')).toBe(false);
|
||||
});
|
||||
|
||||
it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is not set', () => {
|
||||
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
|
||||
const service = FeatureFlagService.fromEnv();
|
||||
|
||||
expect(service.isEnabled('driver_profiles')).toBe(false);
|
||||
expect(service.isEnabled('team_profiles')).toBe(false);
|
||||
expect(service.isEnabled('wallets')).toBe(false);
|
||||
expect(service.isEnabled('sponsors')).toBe(false);
|
||||
expect(service.isEnabled('team_feature')).toBe(false);
|
||||
expect(service.isEnabled('alpha_features')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow FEATURE_FLAGS to override alpha mode', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
|
||||
process.env.FEATURE_FLAGS = 'driver_profiles,wallets';
|
||||
|
||||
const service = FeatureFlagService.fromEnv();
|
||||
|
||||
expect(service.isEnabled('driver_profiles')).toBe(true);
|
||||
expect(service.isEnabled('wallets')).toBe(true);
|
||||
expect(service.isEnabled('team_profiles')).toBe(false);
|
||||
expect(service.isEnabled('sponsors')).toBe(false);
|
||||
expect(service.isEnabled('team_feature')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return correct list of enabled flags in alpha mode', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
|
||||
it('should use default localhost URL when NEXT_PUBLIC_API_BASE_URL is not set', async () => {
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
const service = FeatureFlagService.fromEnv();
|
||||
const enabledFlags = service.getEnabledFlags();
|
||||
|
||||
expect(enabledFlags).toContain('driver_profiles');
|
||||
expect(enabledFlags).toContain('team_profiles');
|
||||
expect(enabledFlags).toContain('wallets');
|
||||
expect(enabledFlags).toContain('sponsors');
|
||||
expect(enabledFlags).toContain('team_feature');
|
||||
expect(enabledFlags).toContain('alpha_features');
|
||||
expect(enabledFlags.length).toBe(6);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user