feature flags

This commit is contained in:
2026-01-07 22:05:53 +01:00
parent 1b63fa646c
commit 606b64cec7
530 changed files with 2092 additions and 2943 deletions

View File

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