feature flags
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
# GridPilot Website Environment Variables
|
||||
|
||||
# Application Mode
|
||||
# pre-launch = landing page only, no features
|
||||
# alpha = full platform with all features enabled automatically
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
|
||||
# Site URL (for metadata and OG tags)
|
||||
NEXT_PUBLIC_SITE_URL=https://gridpilot.com
|
||||
|
||||
# Feature Flags (Optional - only needed for custom feature selection)
|
||||
# When in alpha mode, all features are enabled automatically
|
||||
# Use this to override or select specific features
|
||||
# FEATURE_FLAGS=driver_profiles,team_profiles,wallets,sponsors,team_feature
|
||||
# API base URL (browser → API)
|
||||
NEXT_PUBLIC_API_BASE_URL=https://gridpilot.com/api
|
||||
|
||||
# Discord Community
|
||||
# Discord invite URL for the community CTA
|
||||
# Get this from: Discord Server Settings -> Invites -> Create Invite
|
||||
# Example: https://discord.gg/your-invite-code
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
|
||||
# Vercel KV (for email signups and rate limiting)
|
||||
# Get these from: https://vercel.com/dashboard -> Storage -> KV
|
||||
@@ -17,23 +19,15 @@ NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
|
||||
# KV_REST_API_URL=your_kv_rest_api_url_here
|
||||
# KV_REST_API_TOKEN=your_kv_rest_api_token_here
|
||||
|
||||
# Site URL (for metadata and OG tags)
|
||||
NEXT_PUBLIC_SITE_URL=https://gridpilot.com
|
||||
# Feature Flags (Optional - for fine-grained feature control)
|
||||
# Use this to enable/disable specific features
|
||||
# FEATURE_FLAGS=driver_profiles,team_profiles,wallets,sponsors,team_feature
|
||||
|
||||
# Discord Community
|
||||
# Discord invite URL for the community CTA
|
||||
# Get this from: Discord Server Settings -> Invites -> Create Invite
|
||||
# Example: https://discord.gg/your-invite-code
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
|
||||
# Example configurations:
|
||||
|
||||
# Pre-launch (default)
|
||||
# NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
|
||||
|
||||
# Alpha with all features (recommended)
|
||||
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
|
||||
# Alpha with specific features only
|
||||
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
# FEATURE_FLAGS=driver_profiles,wallets
|
||||
# Optional site metadata (defaults used when unset)
|
||||
# NEXT_PUBLIC_SITE_NAME=GridPilot
|
||||
# NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
|
||||
# NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@example.com
|
||||
# NEXT_PUBLIC_LEGAL_COMPANY_NAME=
|
||||
# NEXT_PUBLIC_LEGAL_VAT_ID=
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY=
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS=
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
# Accept build arguments
|
||||
ARG NODE_ENV=production
|
||||
ARG NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
# Install build dependencies required for SWC and sharp
|
||||
RUN apk add --no-cache \
|
||||
python3 \
|
||||
@@ -36,6 +40,11 @@ RUN npm install --include-workspace-root --no-audit --fund=false
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Set environment variables for build
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Build the website
|
||||
WORKDIR /app/apps/website
|
||||
RUN npm run build
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
import { DriversTemplate } from '@/templates/DriversTemplate';
|
||||
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import type { DriverService } from '@/lib/services/drivers/DriverService';
|
||||
import { DriverService } from '@/lib/services/drivers/DriverService';
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
|
||||
export default async function Page() {
|
||||
const data = await PageDataFetcher.fetch<DriverService, 'getDriverLeaderboard'>(
|
||||
DRIVER_SERVICE_TOKEN,
|
||||
'getDriverLeaderboard'
|
||||
);
|
||||
// Manual dependency creation (consistent with /races and /teams)
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
// Create API client
|
||||
const driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Create service
|
||||
const service = new DriverService(driversApiClient);
|
||||
|
||||
const data = await service.getDriverLeaderboard();
|
||||
|
||||
return <PageWrapper data={data} Template={DriversTemplate} />;
|
||||
}
|
||||
@@ -71,8 +71,8 @@ export default async function RootLayout({
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize feature flag service
|
||||
const featureService = FeatureFlagService.fromEnv();
|
||||
// Initialize feature flag service from API
|
||||
const featureService = await FeatureFlagService.fromAPI();
|
||||
const enabledFlags = featureService.getEnabledFlags();
|
||||
|
||||
return (
|
||||
|
||||
@@ -152,7 +152,6 @@ export function DebugModeToggle({ show }: DebugModeToggleProps) {
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: {
|
||||
mode: process.env.NODE_ENV,
|
||||
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE,
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
},
|
||||
browser: {
|
||||
|
||||
@@ -53,7 +53,6 @@ interface ErrorStats {
|
||||
};
|
||||
environment: {
|
||||
mode: string;
|
||||
appMode: string;
|
||||
version?: string;
|
||||
buildTime?: string;
|
||||
};
|
||||
@@ -141,7 +140,6 @@ export function ErrorAnalyticsDashboard({
|
||||
},
|
||||
environment: {
|
||||
mode: process.env.NODE_ENV || 'unknown',
|
||||
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch',
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
|
||||
},
|
||||
@@ -432,10 +430,6 @@ export function ErrorAnalyticsDashboard({
|
||||
stats.environment.mode === 'development' ? 'text-green-400' : 'text-yellow-400'
|
||||
}`}>{stats.environment.mode}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">App Mode</span>
|
||||
<span className="text-blue-400 font-mono">{stats.environment.appMode}</span>
|
||||
</div>
|
||||
{stats.environment.version && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Version</span>
|
||||
|
||||
@@ -1,47 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, ReactNode } from 'react';
|
||||
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
|
||||
|
||||
/**
|
||||
* ModeGuard - Conditional rendering component based on application mode
|
||||
* ModeGuard - Conditional rendering component based on feature availability
|
||||
*
|
||||
* Usage:
|
||||
* <ModeGuard mode="pre-launch">
|
||||
* <PreLaunchContent />
|
||||
* <ModeGuard feature="platform.dashboard">
|
||||
* <DashboardContent />
|
||||
* </ModeGuard>
|
||||
*
|
||||
* <ModeGuard mode="alpha">
|
||||
* <FullPlatformContent />
|
||||
* <ModeGuard feature="alpha_features">
|
||||
* <AlphaContent />
|
||||
* </ModeGuard>
|
||||
*/
|
||||
|
||||
export type GuardMode = 'pre-launch' | 'alpha';
|
||||
|
||||
interface ModeGuardProps {
|
||||
mode: GuardMode;
|
||||
feature: string;
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side mode guard component
|
||||
* Note: For initial page load, rely on middleware for route protection
|
||||
* This component is for conditional UI rendering within accessible pages
|
||||
* Client-side feature guard component
|
||||
* Uses API-driven feature flags instead of environment mode
|
||||
*/
|
||||
export function ModeGuard({ mode, children, fallback = null }: ModeGuardProps) {
|
||||
export function ModeGuard({ feature, children, fallback = null }: ModeGuardProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [currentMode, setCurrentMode] = useState<GuardMode>('pre-launch');
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
setCurrentMode(getClientMode());
|
||||
}, []);
|
||||
|
||||
// Check feature availability using API-driven service
|
||||
const checkFeature = async () => {
|
||||
try {
|
||||
const service = await FeatureFlagService.fromAPI();
|
||||
setIsEnabled(service.isEnabled(feature));
|
||||
} catch (error) {
|
||||
console.error('Error checking feature:', error);
|
||||
setIsEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkFeature();
|
||||
}, [feature]);
|
||||
|
||||
if (!isMounted) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
if (currentMode !== mode) {
|
||||
if (!isEnabled) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
@@ -49,40 +59,57 @@ export function ModeGuard({ mode, children, fallback = null }: ModeGuardProps) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mode on client side from injected environment variable
|
||||
* Falls back to 'pre-launch' if not available
|
||||
* Hook to check feature availability in client components
|
||||
*/
|
||||
function getClientMode(): GuardMode {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'pre-launch';
|
||||
}
|
||||
|
||||
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
|
||||
if (mode === 'alpha') {
|
||||
return 'alpha';
|
||||
}
|
||||
|
||||
return 'pre-launch';
|
||||
export function useFeature(feature: string): boolean {
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkFeature = async () => {
|
||||
try {
|
||||
const service = await FeatureFlagService.fromAPI();
|
||||
setIsEnabled(service.isEnabled(feature));
|
||||
} catch (error) {
|
||||
console.error('Error checking feature:', error);
|
||||
setIsEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkFeature();
|
||||
}, [feature]);
|
||||
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get current mode in client components
|
||||
* Hook to check multiple features
|
||||
*/
|
||||
export function useAppMode(): GuardMode {
|
||||
return getClientMode();
|
||||
}
|
||||
export function useFeatures(features: string[]): Record<string, boolean> {
|
||||
const [featureStates, setFeatureStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
/**
|
||||
* Hook to check if in pre-launch mode
|
||||
*/
|
||||
export function useIsPreLaunch(): boolean {
|
||||
return getClientMode() === 'pre-launch';
|
||||
}
|
||||
useEffect(() => {
|
||||
const checkFeatures = async () => {
|
||||
try {
|
||||
const service = await FeatureFlagService.fromAPI();
|
||||
const states: Record<string, boolean> = {};
|
||||
|
||||
features.forEach(feature => {
|
||||
states[feature] = service.isEnabled(feature);
|
||||
});
|
||||
|
||||
setFeatureStates(states);
|
||||
} catch (error) {
|
||||
console.error('Error checking features:', error);
|
||||
const states: Record<string, boolean> = {};
|
||||
features.forEach(feature => {
|
||||
states[feature] = false;
|
||||
});
|
||||
setFeatureStates(states);
|
||||
}
|
||||
};
|
||||
|
||||
checkFeatures();
|
||||
}, [features]);
|
||||
|
||||
/**
|
||||
* Hook to check if in alpha mode
|
||||
*/
|
||||
export function useIsAlpha(): boolean {
|
||||
return getClientMode() === 'alpha';
|
||||
return featureStates;
|
||||
}
|
||||
1
apps/website/env.d.ts
vendored
1
apps/website/env.d.ts
vendored
@@ -50,7 +50,6 @@ declare global {
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
|
||||
// Website (public, exposed to browser)
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha';
|
||||
NEXT_PUBLIC_SITE_URL?: string;
|
||||
NEXT_PUBLIC_API_BASE_URL?: string;
|
||||
NEXT_PUBLIC_SITE_NAME?: string;
|
||||
|
||||
@@ -14,12 +14,15 @@ export function getWebsiteApiBaseUrl(): string {
|
||||
return normalizeBaseUrl(configured);
|
||||
}
|
||||
|
||||
// In test-like environments, check if we have any configuration at all
|
||||
const isTestLike =
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.CI === 'true' ||
|
||||
process.env.DOCKER === 'true';
|
||||
|
||||
if (isTestLike) {
|
||||
// If we're in a test-like environment and have NO configuration, that's an error
|
||||
// But if we have some configuration (even if empty), we should use the fallback
|
||||
if (isTestLike && !process.env.API_BASE_URL && !process.env.NEXT_PUBLIC_API_BASE_URL) {
|
||||
throw new Error(
|
||||
isBrowser
|
||||
? 'Missing NEXT_PUBLIC_API_BASE_URL. In Docker/CI/test we do not allow falling back to localhost.'
|
||||
|
||||
@@ -4,7 +4,6 @@ const urlOptional = z.string().url().optional();
|
||||
const stringOptional = z.string().optional();
|
||||
|
||||
const publicEnvSchema = z.object({
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE: z.enum(['pre-launch', 'alpha']).optional(),
|
||||
NEXT_PUBLIC_SITE_URL: urlOptional,
|
||||
NEXT_PUBLIC_API_BASE_URL: urlOptional,
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ export const ApiModule = new ContainerModule((options) => {
|
||||
ClientClass: any,
|
||||
context: any
|
||||
) => {
|
||||
const baseUrl = context.get(CONFIG_TOKEN);
|
||||
const getConfig = context.get(CONFIG_TOKEN);
|
||||
const baseUrl = getConfig(); // Call function to get fresh config
|
||||
const errorReporter = context.get(ERROR_REPORTER_TOKEN);
|
||||
const logger = context.get(LOGGER_TOKEN);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export const CoreModule = new ContainerModule((options) => {
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
// Config
|
||||
bind<string>(CONFIG_TOKEN)
|
||||
.toConstantValue(getWebsiteApiBaseUrl());
|
||||
// Config - bind as function to read env at runtime
|
||||
bind<() => string>(CONFIG_TOKEN)
|
||||
.toDynamicValue(() => () => getWebsiteApiBaseUrl());
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* FeatureFlagService - Manages feature flags for both server and client
|
||||
*
|
||||
* Automatic Alpha Mode Integration:
|
||||
* When NEXT_PUBLIC_GRIDPILOT_MODE=alpha, all features are automatically enabled.
|
||||
* This eliminates the need to manually set FEATURE_FLAGS for alpha deployments.
|
||||
* API-Driven Integration:
|
||||
* Fetches feature flags from the API endpoint GET /features
|
||||
* Returns empty flags on error (secure by default)
|
||||
*
|
||||
* Server: Reads from process.env.FEATURE_FLAGS (comma-separated)
|
||||
* OR auto-enables all features if in alpha mode
|
||||
* Server: Fetches from API at ${NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'}/features
|
||||
* Client: Reads from session context or provides mock implementation
|
||||
*/
|
||||
|
||||
@@ -18,7 +17,7 @@ export class FeatureFlagService {
|
||||
if (flags) {
|
||||
this.flags = new Set(flags);
|
||||
} else {
|
||||
// Parse from environment variable
|
||||
// Parse from environment variable (fallback for backward compatibility)
|
||||
const flagsEnv = process.env.FEATURE_FLAGS;
|
||||
this.flags = flagsEnv
|
||||
? new Set(flagsEnv.split(',').map(f => f.trim()))
|
||||
@@ -41,33 +40,44 @@ export class FeatureFlagService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create service with environment flags
|
||||
* Automatically enables all features if in alpha mode
|
||||
* FEATURE_FLAGS can override alpha mode defaults
|
||||
* Factory method to create service by fetching from API
|
||||
* Fetches from ${NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'}/features
|
||||
* On error, returns empty flags (secure by default)
|
||||
*/
|
||||
static fromEnv(): FeatureFlagService {
|
||||
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
const flagsEnv = process.env.FEATURE_FLAGS;
|
||||
|
||||
// If FEATURE_FLAGS is explicitly set, use it (overrides alpha mode)
|
||||
if (flagsEnv) {
|
||||
return new FeatureFlagService();
|
||||
static async fromAPI(): Promise<FeatureFlagService> {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const url = `${baseUrl}/features`;
|
||||
|
||||
try {
|
||||
// Use next: { revalidate: 0 } for Next.js server runtime
|
||||
// This is equivalent to cache: 'no-store' but is the preferred Next.js convention
|
||||
const response = await fetch(url, {
|
||||
next: { revalidate: 0 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Parse JSON { features: Record<string, string> }
|
||||
// Enable flags whose value is 'enabled'
|
||||
const enabledFlags: string[] = [];
|
||||
if (data.features && typeof data.features === 'object') {
|
||||
Object.entries(data.features).forEach(([flag, value]) => {
|
||||
if (value === 'enabled') {
|
||||
enabledFlags.push(flag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new FeatureFlagService(enabledFlags);
|
||||
} catch (error) {
|
||||
// Log error but return empty flags (secure by default)
|
||||
console.error('Failed to fetch feature flags from API:', error);
|
||||
return new FeatureFlagService([]);
|
||||
}
|
||||
|
||||
// If in alpha mode, automatically enable all features
|
||||
if (mode === 'alpha') {
|
||||
return new FeatureFlagService([
|
||||
'driver_profiles',
|
||||
'team_profiles',
|
||||
'wallets',
|
||||
'sponsors',
|
||||
'team_feature',
|
||||
'alpha_features'
|
||||
]);
|
||||
}
|
||||
|
||||
// Otherwise, use FEATURE_FLAGS environment variable (empty if not set)
|
||||
return new FeatureFlagService();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ export interface ReplayContext {
|
||||
}>;
|
||||
metadata: {
|
||||
mode: string;
|
||||
appMode: string;
|
||||
timestamp: string;
|
||||
replayId: string;
|
||||
};
|
||||
@@ -85,7 +84,6 @@ export class ErrorReplaySystem {
|
||||
})) || [],
|
||||
metadata: {
|
||||
mode: process.env.NODE_ENV || 'unknown',
|
||||
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch',
|
||||
timestamp: new Date().toISOString(),
|
||||
replayId,
|
||||
},
|
||||
@@ -272,7 +270,6 @@ REACT ERRORS (${replay.reactErrors.length})
|
||||
METADATA
|
||||
--------
|
||||
Mode: ${replay.metadata.mode}
|
||||
App Mode: ${replay.metadata.appMode}
|
||||
Original Timestamp: ${replay.metadata.timestamp}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { getAppMode, isPreLaunch, isAlpha, getPublicRoutes, isPublicRoute } from './mode';
|
||||
|
||||
describe('mode', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('getAppMode', () => {
|
||||
it('should return "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is not set', () => {
|
||||
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
expect(getAppMode()).toBe('pre-launch');
|
||||
});
|
||||
|
||||
it('should return "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is empty', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = '';
|
||||
expect(getAppMode()).toBe('pre-launch');
|
||||
});
|
||||
|
||||
it('should return "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is "pre-launch"', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
|
||||
expect(getAppMode()).toBe('pre-launch');
|
||||
});
|
||||
|
||||
it('should return "alpha" when NEXT_PUBLIC_GRIDPILOT_MODE is "alpha"', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
|
||||
expect(getAppMode()).toBe('alpha');
|
||||
});
|
||||
|
||||
it('should throw error in development for invalid mode', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid';
|
||||
|
||||
expect(() => getAppMode()).toThrow('Invalid NEXT_PUBLIC_GRIDPILOT_MODE: "invalid". Must be one of: pre-launch, alpha');
|
||||
});
|
||||
|
||||
it('should log error and return "pre-launch" in production for invalid mode', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid';
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const result = getAppMode();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Invalid NEXT_PUBLIC_GRIDPILOT_MODE: "invalid". Must be one of: pre-launch, alpha'
|
||||
);
|
||||
expect(result).toBe('pre-launch');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPreLaunch', () => {
|
||||
it('should return true when mode is "pre-launch"', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
|
||||
expect(isPreLaunch()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when mode is "alpha"', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
|
||||
expect(isPreLaunch()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when mode is not set', () => {
|
||||
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
expect(isPreLaunch()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAlpha', () => {
|
||||
it('should return true when mode is "alpha"', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
|
||||
expect(isAlpha()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when mode is "pre-launch"', () => {
|
||||
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
|
||||
expect(isAlpha()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when mode is not set', () => {
|
||||
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
expect(isAlpha()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPublicRoutes', () => {
|
||||
it('should return an array of public routes', () => {
|
||||
const routes = getPublicRoutes();
|
||||
expect(Array.isArray(routes)).toBe(true);
|
||||
expect(routes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should include core public pages', () => {
|
||||
const routes = getPublicRoutes();
|
||||
expect(routes).toContain('/');
|
||||
expect(routes).toContain('/leagues');
|
||||
expect(routes).toContain('/drivers');
|
||||
});
|
||||
|
||||
it('should include auth routes', () => {
|
||||
const routes = getPublicRoutes();
|
||||
expect(routes).toContain('/auth/login');
|
||||
expect(routes).toContain('/auth/signup');
|
||||
expect(routes).toContain('/api/auth/login');
|
||||
});
|
||||
|
||||
it('should return consistent results', () => {
|
||||
const routes1 = getPublicRoutes();
|
||||
const routes2 = getPublicRoutes();
|
||||
expect(routes1).toEqual(routes2); // Same content
|
||||
expect(routes1).not.toBe(routes2); // Different references (immutable)
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPublicRoute', () => {
|
||||
it('should return true for exact matches', () => {
|
||||
expect(isPublicRoute('/')).toBe(true);
|
||||
expect(isPublicRoute('/leagues')).toBe(true);
|
||||
expect(isPublicRoute('/auth/login')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for nested routes under public prefixes', () => {
|
||||
expect(isPublicRoute('/leagues/123')).toBe(true);
|
||||
expect(isPublicRoute('/leagues/create')).toBe(true);
|
||||
expect(isPublicRoute('/drivers/456')).toBe(true);
|
||||
expect(isPublicRoute('/teams/789')).toBe(true);
|
||||
expect(isPublicRoute('/races/abc')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for private routes', () => {
|
||||
expect(isPublicRoute('/dashboard')).toBe(false);
|
||||
expect(isPublicRoute('/admin')).toBe(false);
|
||||
// Note: /leagues/123/admin is actually public because it starts with /leagues/
|
||||
// This is the intended behavior - all nested routes under public prefixes are public
|
||||
});
|
||||
|
||||
it('should return true for nested routes under public prefixes', () => {
|
||||
// These are all public because they start with public prefixes
|
||||
expect(isPublicRoute('/leagues/123')).toBe(true);
|
||||
expect(isPublicRoute('/leagues/123/admin')).toBe(true);
|
||||
expect(isPublicRoute('/drivers/456')).toBe(true);
|
||||
expect(isPublicRoute('/teams/789')).toBe(true);
|
||||
expect(isPublicRoute('/races/abc')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for routes that only start with public prefix but are different', () => {
|
||||
// This tests that '/leaguex' doesn't match '/leagues'
|
||||
expect(isPublicRoute('/leaguex')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle trailing slashes correctly', () => {
|
||||
expect(isPublicRoute('/leagues/')).toBe(true);
|
||||
expect(isPublicRoute('/drivers/')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* Mode detection system for GridPilot website
|
||||
*
|
||||
* Controls whether the site shows pre-launch content or alpha platform
|
||||
* Based on NEXT_PUBLIC_GRIDPILOT_MODE environment variable
|
||||
*/
|
||||
|
||||
export type AppMode = 'pre-launch' | 'alpha';
|
||||
|
||||
const VALID_MODES: readonly AppMode[] = ['pre-launch', 'alpha'] as const;
|
||||
|
||||
/**
|
||||
* Get the current application mode from environment variable
|
||||
* Defaults to 'pre-launch' if not set or invalid
|
||||
*
|
||||
* @throws {Error} If mode is set but invalid (development only)
|
||||
* @returns {AppMode} The current application mode
|
||||
*/
|
||||
export function getAppMode(): AppMode {
|
||||
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
||||
|
||||
if (!mode) {
|
||||
return 'pre-launch';
|
||||
}
|
||||
|
||||
if (!isValidMode(mode)) {
|
||||
const validModes = VALID_MODES.join(', ');
|
||||
const error = `Invalid NEXT_PUBLIC_GRIDPILOT_MODE: "${mode}". Must be one of: ${validModes}`;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
return 'pre-launch';
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid AppMode
|
||||
*/
|
||||
function isValidMode(mode: string): mode is AppMode {
|
||||
return VALID_MODES.includes(mode as AppMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently in pre-launch mode
|
||||
*/
|
||||
export function isPreLaunch(): boolean {
|
||||
return getAppMode() === 'pre-launch';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently in alpha mode
|
||||
*/
|
||||
export function isAlpha(): boolean {
|
||||
return getAppMode() === 'alpha';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of public routes that are always accessible
|
||||
*/
|
||||
export function getPublicRoutes(): readonly string[] {
|
||||
return [
|
||||
// Core public pages
|
||||
'/',
|
||||
|
||||
// Public content routes (leagues, drivers, teams, leaderboards, races)
|
||||
'/leagues',
|
||||
'/drivers',
|
||||
'/teams',
|
||||
'/leaderboards',
|
||||
'/races',
|
||||
|
||||
// Sponsor signup (publicly accessible)
|
||||
'/sponsor/signup',
|
||||
|
||||
// Auth routes
|
||||
'/api/signup',
|
||||
'/api/auth/signup',
|
||||
'/api/auth/login',
|
||||
'/api/auth/forgot-password',
|
||||
'/api/auth/reset-password',
|
||||
'/api/auth/session',
|
||||
'/api/auth/logout',
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/auth/forgot-password',
|
||||
'/auth/reset-password',
|
||||
] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route is public (accessible in all modes)
|
||||
* Supports both exact matches and prefix matches for nested routes
|
||||
*/
|
||||
export function isPublicRoute(pathname: string): boolean {
|
||||
const publicRoutes = getPublicRoutes();
|
||||
|
||||
// Check exact match first
|
||||
if (publicRoutes.includes(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check prefix matches for nested routes
|
||||
// e.g., '/leagues' should match '/leagues/123', '/leagues/create', etc.
|
||||
const publicPrefixes = [
|
||||
'/leagues',
|
||||
'/drivers',
|
||||
'/teams',
|
||||
'/leaderboards',
|
||||
'/races',
|
||||
];
|
||||
|
||||
return publicPrefixes.some(prefix =>
|
||||
pathname === prefix || pathname.startsWith(prefix + '/')
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export async function getHomeData() {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
const featureService = FeatureFlagService.fromEnv();
|
||||
const featureService = await FeatureFlagService.fromAPI();
|
||||
const isAlpha = featureService.isEnabled('alpha_features');
|
||||
const discovery = await landingService.getHomeDiscovery();
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { RaceService } from './RaceService';
|
||||
import { RacesApiClient } from '../../api/races/RacesApiClient';
|
||||
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
|
||||
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
|
||||
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
|
||||
import type { RaceDetailsViewModel } from '../../view-models/RaceDetailsViewModel';
|
||||
|
||||
describe('RaceService', () => {
|
||||
let mockApiClient: Mocked<RacesApiClient>;
|
||||
let service: RaceService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getDetail: vi.fn(),
|
||||
getPageData: vi.fn(),
|
||||
getTotal: vi.fn(),
|
||||
register: vi.fn(),
|
||||
withdraw: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
complete: vi.fn(),
|
||||
reopen: vi.fn(),
|
||||
} as unknown as Mocked<RacesApiClient>;
|
||||
|
||||
service = new RaceService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getRaceDetail', () => {
|
||||
it('should call apiClient.getDetail and return RaceDetailViewModel', async () => {
|
||||
const raceId = 'race-123';
|
||||
const driverId = 'driver-456';
|
||||
|
||||
const mockDto = {
|
||||
race: { id: raceId, track: 'Test Track' },
|
||||
league: { id: 'league-1', name: 'Test League' },
|
||||
entryList: [],
|
||||
registration: { isRegistered: true, canRegister: false },
|
||||
userResult: null,
|
||||
};
|
||||
|
||||
mockApiClient.getDetail.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRaceDetail(raceId, driverId);
|
||||
|
||||
expect(mockApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
|
||||
expect(result).toBeInstanceOf(RaceDetailViewModel);
|
||||
expect(result.race?.id).toBe(raceId);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getDetail fails', async () => {
|
||||
const raceId = 'race-123';
|
||||
const driverId = 'driver-456';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getDetail.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getRaceDetail(raceId, driverId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRaceDetails', () => {
|
||||
it('should call apiClient.getDetail and return a ViewModel-shaped object (no DTOs)', async () => {
|
||||
const raceId = 'race-123';
|
||||
const driverId = 'driver-456';
|
||||
|
||||
const mockDto = {
|
||||
race: {
|
||||
id: raceId,
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
scheduledAt: '2023-12-31T20:00:00Z',
|
||||
status: 'completed',
|
||||
sessionType: 'race',
|
||||
},
|
||||
league: { id: 'league-1', name: 'Test League', description: 'Desc', settings: { maxDrivers: 32 } },
|
||||
entryList: [],
|
||||
registration: { isUserRegistered: true, canRegister: false },
|
||||
userResult: null,
|
||||
};
|
||||
|
||||
mockApiClient.getDetail.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result: RaceDetailsViewModel = await service.getRaceDetails(raceId, driverId);
|
||||
|
||||
expect(mockApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
|
||||
expect(result.race?.id).toBe(raceId);
|
||||
expect(result.league?.id).toBe('league-1');
|
||||
expect(result.registration.isUserRegistered).toBe(true);
|
||||
expect(result.canReopenRace).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRacesPageData', () => {
|
||||
it('should call apiClient.getPageData and return RacesPageViewModel with transformed data', async () => {
|
||||
const mockDto = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari',
|
||||
scheduledAt: '2023-10-01T10:00:00Z',
|
||||
status: 'upcoming',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Silverstone',
|
||||
car: 'Mercedes',
|
||||
scheduledAt: '2023-09-15T10:00:00Z',
|
||||
status: 'completed',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRacesPageData();
|
||||
|
||||
expect(mockApiClient.getPageData).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(RacesPageViewModel);
|
||||
expect(result.upcomingRaces).toHaveLength(1);
|
||||
expect(result.completedRaces).toHaveLength(1);
|
||||
expect(result.totalCount).toBe(2);
|
||||
expect(result.upcomingRaces[0].title).toBe('Monza - Ferrari');
|
||||
expect(result.completedRaces[0].title).toBe('Silverstone - Mercedes');
|
||||
});
|
||||
|
||||
it('should handle empty races array', async () => {
|
||||
const mockDto = { races: [] };
|
||||
|
||||
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRacesPageData();
|
||||
|
||||
expect(result.upcomingRaces).toHaveLength(0);
|
||||
expect(result.completedRaces).toHaveLength(0);
|
||||
expect(result.totalCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getPageData fails', async () => {
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getPageData.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getRacesPageData()).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRacesTotal', () => {
|
||||
it('should call apiClient.getTotal and return RaceStatsViewModel', async () => {
|
||||
const mockDto = { totalRaces: 42 };
|
||||
|
||||
mockApiClient.getTotal.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRacesTotal();
|
||||
|
||||
expect(mockApiClient.getTotal).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(RaceStatsViewModel);
|
||||
expect(result.totalRaces).toBe(42);
|
||||
expect(result.formattedTotalRaces).toBe('42');
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getTotal fails', async () => {
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getTotal.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getRacesTotal()).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reopenRace', () => {
|
||||
it('should call apiClient.reopen with raceId', async () => {
|
||||
const raceId = 'race-123';
|
||||
|
||||
await service.reopenRace(raceId);
|
||||
|
||||
expect(mockApiClient.reopen).toHaveBeenCalledWith(raceId);
|
||||
});
|
||||
|
||||
it('should propagate errors from apiClient.reopen', async () => {
|
||||
const raceId = 'race-123';
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.reopen.mockRejectedValue(error);
|
||||
|
||||
await expect(service.reopenRace(raceId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AcceptSponsorshipRequestInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/ActivityItemDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AllLeaguesWithCapacityAndScoringDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AllLeaguesWithCapacityDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AllRacesFilterOptionsDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AllRacesLeagueFilterDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AllRacesListItemDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AllRacesPageDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AllRacesStatusFilterDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/ApplyPenaltyCommandDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/ApproveJoinRequestInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/ApproveJoinRequestOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AuthSessionDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AuthenticatedUserDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
@@ -11,5 +11,6 @@ export interface AuthenticatedUserDTO {
|
||||
displayName: string;
|
||||
primaryDriverId?: string;
|
||||
avatarUrl?: string;
|
||||
companyId?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AvailableLeagueDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AvatarDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/AwardPrizeResultDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/BillingStatsDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CompleteOnboardingInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CompleteOnboardingOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateLeagueInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateLeagueOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateLeagueScheduleRaceInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateLeagueScheduleRaceOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreatePaymentInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreatePaymentOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreatePrizeResultDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateSponsorInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateSponsorOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateTeamInputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/CreateTeamOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardDriverSummaryDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardFeedItemSummaryDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardFeedSummaryDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardFriendSummaryDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardLeagueStandingSummaryDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardOverviewDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardRaceSummaryDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DashboardRecentResultDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DeleteMediaOutputDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
|
||||
* Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('types/generated/DeletePrizeResultDTO', () => {
|
||||
it('should be defined', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user