feature flags
This commit is contained in:
@@ -39,9 +39,9 @@ interface OpenAPISpec {
|
||||
}
|
||||
|
||||
describe('API Contract Validation', () => {
|
||||
const apiRoot = path.join(__dirname, '../../..');
|
||||
const openapiPath = path.join(apiRoot, 'openapi.json');
|
||||
const generatedTypesDir = path.join(apiRoot, '../website/lib/types/generated');
|
||||
const apiRoot = path.join(__dirname, '../..'); // /Users/marcmintel/Projects/gridpilot
|
||||
const openapiPath = path.join(apiRoot, 'apps/api/openapi.json');
|
||||
const generatedTypesDir = path.join(apiRoot, 'apps/website/lib/types/generated');
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
describe('OpenAPI Spec Integrity', () => {
|
||||
@@ -68,7 +68,7 @@ describe('API Contract Validation', () => {
|
||||
});
|
||||
|
||||
it('committed openapi.json should match generator output', async () => {
|
||||
const repoRoot = path.join(apiRoot, '../..');
|
||||
const repoRoot = apiRoot; // Already at the repo root
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gridpilot-openapi-'));
|
||||
const generatedOpenapiPath = path.join(tmpDir, 'openapi.json');
|
||||
|
||||
@@ -5,6 +5,83 @@ import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
|
||||
const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
// Wait for API to be ready with seeded data before running tests
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
|
||||
|
||||
console.log('[SETUP] Waiting for API to be ready...');
|
||||
|
||||
// Poll the API until it returns data (indicating seeding is complete)
|
||||
const maxAttempts = 60;
|
||||
const interval = 1000; // 1 second
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
// Try to fetch total drivers count - this endpoint should return > 0 after seeding
|
||||
const response = await request.get(`${API_BASE_URL}/drivers/total-drivers`);
|
||||
|
||||
if (response.ok()) {
|
||||
const data = await response.json();
|
||||
|
||||
// Check if we have actual drivers (count > 0)
|
||||
if (data && data.totalDrivers && data.totalDrivers > 0) {
|
||||
console.log(`[SETUP] API is ready with ${data.totalDrivers} drivers`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: API not ready yet (status: ${response.status()})`);
|
||||
} catch (error) {
|
||||
console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: ${error.message}`);
|
||||
}
|
||||
|
||||
// Wait before next attempt
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
throw new Error('[SETUP] API failed to become ready with seeded data within timeout');
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to fetch feature flags from the API
|
||||
* Uses Playwright request context for compatibility across environments
|
||||
*/
|
||||
async function fetchFeatureFlags(request: import('@playwright/test').APIRequestContext): Promise<{ features: Record<string, string>; timestamp: string }> {
|
||||
const apiBaseUrl = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
|
||||
const featuresUrl = `${apiBaseUrl}/features`;
|
||||
|
||||
try {
|
||||
const response = await request.get(featuresUrl);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`[FEATURE FLAGS] Failed to fetch from ${featuresUrl}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to compute enabled flags from feature config
|
||||
*/
|
||||
function getEnabledFlags(featureData: { features: Record<string, string> }): string[] {
|
||||
if (!featureData.features || typeof featureData.features !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(featureData.features)
|
||||
.filter(([, value]) => value === 'enabled')
|
||||
.map(([flag]) => flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if a specific flag is enabled
|
||||
*/
|
||||
function isFeatureEnabled(featureData: { features: Record<string, string> }, flag: string): boolean {
|
||||
return featureData.features?.[flag] === 'enabled';
|
||||
}
|
||||
|
||||
test.describe('Website Pages - TypeORM Integration', () => {
|
||||
let routeManager: WebsiteRouteManager;
|
||||
|
||||
@@ -611,4 +688,148 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
expect(detailBodyText?.length).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
// ==================== FEATURE FLAG TESTS ====================
|
||||
// These tests validate API-driven feature flags
|
||||
|
||||
test('features endpoint returns valid contract and reachable from API', async ({ request }) => {
|
||||
// Contract test: verify /features endpoint returns correct shape
|
||||
const featureData = await fetchFeatureFlags(request);
|
||||
|
||||
// Verify contract: { features: object, timestamp: string }
|
||||
expect(featureData).toHaveProperty('features');
|
||||
expect(featureData).toHaveProperty('timestamp');
|
||||
|
||||
// Verify features is an object
|
||||
expect(typeof featureData.features).toBe('object');
|
||||
expect(featureData.features).not.toBeNull();
|
||||
|
||||
// Verify timestamp is a string (ISO format)
|
||||
expect(typeof featureData.timestamp).toBe('string');
|
||||
expect(featureData.timestamp.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify at least one feature exists (basic sanity check)
|
||||
const featureKeys = Object.keys(featureData.features);
|
||||
expect(featureKeys.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify all feature values are valid states
|
||||
const validStates = ['enabled', 'disabled', 'coming_soon', 'hidden'];
|
||||
Object.values(featureData.features).forEach(value => {
|
||||
expect(validStates).toContain(value);
|
||||
});
|
||||
|
||||
console.log(`[FEATURE TEST] API features endpoint verified: ${featureKeys.length} flags loaded`);
|
||||
});
|
||||
|
||||
test('conditional UI rendering based on feature flags', async ({ page, request }) => {
|
||||
// Fetch current feature flags from API
|
||||
const featureData = await fetchFeatureFlags(request);
|
||||
const enabledFlags = getEnabledFlags(featureData);
|
||||
|
||||
console.log(`[FEATURE TEST] Enabled flags: ${enabledFlags.join(', ')}`);
|
||||
|
||||
// Test 1: Verify beta features are conditionally rendered
|
||||
// Check if beta.newUI feature affects UI
|
||||
const betaNewUIEnabled = isFeatureEnabled(featureData, 'beta.newUI');
|
||||
|
||||
// Navigate to a page that might have beta features
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}/dashboard`);
|
||||
expect(response?.ok()).toBe(true);
|
||||
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toBeTruthy();
|
||||
|
||||
// If beta.newUI is enabled, we should see beta UI elements
|
||||
// If disabled, beta elements should be absent
|
||||
if (betaNewUIEnabled) {
|
||||
console.log('[FEATURE TEST] beta.newUI is enabled - checking for beta UI elements');
|
||||
// Beta UI might have specific markers - check for common beta indicators
|
||||
const hasBetaIndicators = bodyText?.includes('beta') ||
|
||||
bodyText?.includes('Beta') ||
|
||||
bodyText?.includes('NEW') ||
|
||||
bodyText?.includes('experimental');
|
||||
// Beta features may or may not be visible depending on implementation
|
||||
// This test validates the flag is being read correctly
|
||||
// We don't assert on hasBetaIndicators since beta UI may not be implemented yet
|
||||
console.log(`[FEATURE TEST] Beta indicators found: ${hasBetaIndicators}`);
|
||||
} else {
|
||||
console.log('[FEATURE TEST] beta.newUI is disabled - verifying beta UI is absent');
|
||||
// If disabled, ensure no beta indicators are present
|
||||
const hasBetaIndicators = bodyText?.includes('beta') ||
|
||||
bodyText?.includes('Beta') ||
|
||||
bodyText?.includes('experimental');
|
||||
// Beta UI should not be visible when disabled
|
||||
expect(hasBetaIndicators).toBe(false);
|
||||
}
|
||||
|
||||
// Test 2: Verify platform features are enabled
|
||||
const platformFeatures = ['platform.leagues', 'platform.teams', 'platform.drivers'];
|
||||
platformFeatures.forEach(flag => {
|
||||
const isEnabled = isFeatureEnabled(featureData, flag);
|
||||
expect(isEnabled).toBe(true); // Should be enabled in test environment
|
||||
});
|
||||
});
|
||||
|
||||
test('feature flag state drives UI behavior', async ({ page, request }) => {
|
||||
// This test validates that feature flags actually control UI visibility
|
||||
const featureData = await fetchFeatureFlags(request);
|
||||
|
||||
// Test sponsor management feature
|
||||
const sponsorManagementEnabled = isFeatureEnabled(featureData, 'sponsors.management');
|
||||
|
||||
// Navigate to sponsor-related area
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}/sponsor/dashboard`);
|
||||
|
||||
// If sponsor management is disabled, we should be redirected or see access denied
|
||||
if (!sponsorManagementEnabled) {
|
||||
// Should redirect away or show access denied
|
||||
const currentUrl = page.url();
|
||||
const isRedirected = !currentUrl.includes('/sponsor/dashboard');
|
||||
|
||||
if (isRedirected) {
|
||||
console.log('[FEATURE TEST] Sponsor management disabled - user redirected as expected');
|
||||
} else {
|
||||
// If not redirected, should show access denied message
|
||||
const bodyText = await page.textContent('body');
|
||||
const hasAccessDenied = bodyText?.includes('disabled') ||
|
||||
bodyText?.includes('unavailable') ||
|
||||
bodyText?.includes('not available');
|
||||
expect(hasAccessDenied).toBe(true);
|
||||
}
|
||||
} else {
|
||||
// Should be able to access sponsor dashboard
|
||||
expect(response?.ok()).toBe(true);
|
||||
console.log('[FEATURE TEST] Sponsor management enabled - dashboard accessible');
|
||||
}
|
||||
});
|
||||
|
||||
test('feature flags are consistent across environments', async ({ request }) => {
|
||||
// This test validates that the same feature endpoint works in both local dev and docker e2e
|
||||
const featureData = await fetchFeatureFlags(request);
|
||||
|
||||
// Verify the API base URL is correctly resolved
|
||||
const apiBaseUrl = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
|
||||
console.log(`[FEATURE TEST] Using API base URL: ${apiBaseUrl}`);
|
||||
|
||||
// Verify we got valid data
|
||||
expect(featureData.features).toBeDefined();
|
||||
expect(Object.keys(featureData.features).length).toBeGreaterThan(0);
|
||||
|
||||
// In test environment, core features should be enabled
|
||||
const requiredFeatures = [
|
||||
'platform.dashboard',
|
||||
'platform.leagues',
|
||||
'platform.teams',
|
||||
'platform.drivers',
|
||||
'platform.races',
|
||||
'platform.leaderboards'
|
||||
];
|
||||
|
||||
requiredFeatures.forEach(flag => {
|
||||
const isEnabled = isFeatureEnabled(featureData, flag);
|
||||
expect(isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
console.log('[FEATURE TEST] All required platform features are enabled');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { createContainer } from '../../apps/website/lib/di/container';
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
||||
import { createContainer, ContainerManager } from '../../apps/website/lib/di/container';
|
||||
import {
|
||||
SESSION_SERVICE_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_SERVICE_TOKEN,
|
||||
@@ -18,157 +18,292 @@ import {
|
||||
RACE_API_CLIENT_TOKEN,
|
||||
} from '../../apps/website/lib/di/tokens';
|
||||
|
||||
// Define minimal service interfaces for testing
|
||||
interface MockSessionService {
|
||||
getSession: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface MockAuthService {
|
||||
login: (email: string, password: string) => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface MockLeagueService {
|
||||
getAllLeagues: () => Promise<unknown[]>;
|
||||
}
|
||||
|
||||
interface MockLeagueMembershipService {
|
||||
getLeagueMemberships: (userId: string) => Promise<unknown[]>;
|
||||
}
|
||||
|
||||
interface ConfigFunction {
|
||||
(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration test for website DI container
|
||||
*
|
||||
* This test verifies that all critical DI bindings are properly configured
|
||||
* and that the container can resolve all required services without throwing
|
||||
* binding errors or missing metadata errors.
|
||||
*
|
||||
* This is a fast, non-Playwright test that runs in CI to catch DI issues early.
|
||||
*/
|
||||
describe('Website DI Container Integration', () => {
|
||||
let container: ReturnType<typeof createContainer>;
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeAll(() => {
|
||||
// Save original environment
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Set up minimal environment for DI container to work
|
||||
// The container needs API_BASE_URL to initialize
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:3001';
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// Create the container once for all tests
|
||||
container = createContainer();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
ContainerManager.getInstance().dispose();
|
||||
});
|
||||
|
||||
it('creates container successfully', () => {
|
||||
expect(container).toBeDefined();
|
||||
expect(container).not.toBeNull();
|
||||
beforeEach(() => {
|
||||
// Clean up all API URL env vars before each test
|
||||
delete process.env.API_BASE_URL;
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
ContainerManager.getInstance().dispose();
|
||||
});
|
||||
|
||||
it('resolves core services without errors', () => {
|
||||
// Core services that should always be available
|
||||
expect(() => container.get(LOGGER_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(CONFIG_TOKEN)).not.toThrow();
|
||||
|
||||
const logger = container.get(LOGGER_TOKEN);
|
||||
const config = container.get(CONFIG_TOKEN);
|
||||
|
||||
expect(logger).toBeDefined();
|
||||
expect(config).toBeDefined();
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
delete process.env.API_BASE_URL;
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
ContainerManager.getInstance().dispose();
|
||||
});
|
||||
|
||||
it('resolves API clients without errors', () => {
|
||||
// API clients that services depend on
|
||||
const apiClients = [
|
||||
LEAGUE_API_CLIENT_TOKEN,
|
||||
AUTH_API_CLIENT_TOKEN,
|
||||
DRIVER_API_CLIENT_TOKEN,
|
||||
TEAM_API_CLIENT_TOKEN,
|
||||
RACE_API_CLIENT_TOKEN,
|
||||
];
|
||||
describe('Basic Container Functionality', () => {
|
||||
it('creates container successfully', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
expect(container).toBeDefined();
|
||||
expect(container).not.toBeNull();
|
||||
});
|
||||
|
||||
for (const token of apiClients) {
|
||||
expect(() => container.get(token)).not.toThrow();
|
||||
const client = container.get(token);
|
||||
expect(client).toBeDefined();
|
||||
}
|
||||
});
|
||||
it('resolves core services without errors', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(LOGGER_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(CONFIG_TOKEN)).not.toThrow();
|
||||
|
||||
const logger = container.get(LOGGER_TOKEN);
|
||||
const config = container.get(CONFIG_TOKEN);
|
||||
|
||||
expect(logger).toBeDefined();
|
||||
expect(config).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves auth services including SessionService (critical for Symbol(Service.Session))', () => {
|
||||
// This specifically tests for the Symbol(Service.Session) binding issue
|
||||
expect(() => container.get(SESSION_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(AUTH_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const sessionService = container.get(SESSION_SERVICE_TOKEN);
|
||||
const authService = container.get(AUTH_SERVICE_TOKEN);
|
||||
|
||||
expect(sessionService).toBeDefined();
|
||||
expect(authService).toBeDefined();
|
||||
|
||||
// Verify the services have expected methods
|
||||
expect(typeof sessionService.getSession).toBe('function');
|
||||
expect(typeof authService.login).toBe('function');
|
||||
});
|
||||
it('resolves API clients without errors', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
const apiClients = [
|
||||
LEAGUE_API_CLIENT_TOKEN,
|
||||
AUTH_API_CLIENT_TOKEN,
|
||||
DRIVER_API_CLIENT_TOKEN,
|
||||
TEAM_API_CLIENT_TOKEN,
|
||||
RACE_API_CLIENT_TOKEN,
|
||||
];
|
||||
|
||||
it('resolves league services including LeagueMembershipService (critical for metadata)', () => {
|
||||
// This specifically tests for the LeagueMembershipService metadata issue
|
||||
expect(() => container.get(LEAGUE_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(LEAGUE_MEMBERSHIP_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const leagueService = container.get(LEAGUE_SERVICE_TOKEN);
|
||||
const membershipService = container.get(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
|
||||
|
||||
expect(leagueService).toBeDefined();
|
||||
expect(membershipService).toBeDefined();
|
||||
|
||||
// Verify the services have expected methods
|
||||
expect(typeof leagueService.getAllLeagues).toBe('function');
|
||||
expect(typeof membershipService.getLeagueMemberships).toBe('function');
|
||||
});
|
||||
|
||||
it('resolves domain services without errors', () => {
|
||||
// Test other critical domain services
|
||||
expect(() => container.get(DRIVER_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(TEAM_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(RACE_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(DASHBOARD_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const driverService = container.get(DRIVER_SERVICE_TOKEN);
|
||||
const teamService = container.get(TEAM_SERVICE_TOKEN);
|
||||
const raceService = container.get(RACE_SERVICE_TOKEN);
|
||||
const dashboardService = container.get(DASHBOARD_SERVICE_TOKEN);
|
||||
|
||||
expect(driverService).toBeDefined();
|
||||
expect(teamService).toBeDefined();
|
||||
expect(raceService).toBeDefined();
|
||||
expect(dashboardService).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves all services in a single operation (full container boot simulation)', () => {
|
||||
// This simulates what happens when the website boots and needs multiple services
|
||||
const tokens = [
|
||||
SESSION_SERVICE_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_SERVICE_TOKEN,
|
||||
LEAGUE_SERVICE_TOKEN,
|
||||
AUTH_SERVICE_TOKEN,
|
||||
DRIVER_SERVICE_TOKEN,
|
||||
TEAM_SERVICE_TOKEN,
|
||||
RACE_SERVICE_TOKEN,
|
||||
DASHBOARD_SERVICE_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
CONFIG_TOKEN,
|
||||
];
|
||||
|
||||
// Resolve all tokens - if any binding is missing or metadata is wrong, this will throw
|
||||
const services = tokens.map(token => {
|
||||
try {
|
||||
return container.get(token);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to resolve token ${token.toString()}: ${error.message}`);
|
||||
for (const token of apiClients) {
|
||||
expect(() => container.get(token)).not.toThrow();
|
||||
const client = container.get(token);
|
||||
expect(client).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
// Verify all services were resolved
|
||||
expect(services.length).toBe(tokens.length);
|
||||
services.forEach((service, index) => {
|
||||
expect(service).toBeDefined();
|
||||
expect(service).not.toBeNull();
|
||||
it('resolves auth services including SessionService', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(SESSION_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(AUTH_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const sessionService = container.get<MockSessionService>(SESSION_SERVICE_TOKEN);
|
||||
const authService = container.get<MockAuthService>(AUTH_SERVICE_TOKEN);
|
||||
|
||||
expect(sessionService).toBeDefined();
|
||||
expect(authService).toBeDefined();
|
||||
expect(typeof sessionService.getSession).toBe('function');
|
||||
expect(typeof authService.login).toBe('function');
|
||||
});
|
||||
|
||||
it('resolves league services including LeagueMembershipService', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(LEAGUE_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(LEAGUE_MEMBERSHIP_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const leagueService = container.get<MockLeagueService>(LEAGUE_SERVICE_TOKEN);
|
||||
const membershipService = container.get<MockLeagueMembershipService>(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
|
||||
|
||||
expect(leagueService).toBeDefined();
|
||||
expect(membershipService).toBeDefined();
|
||||
expect(typeof leagueService.getAllLeagues).toBe('function');
|
||||
expect(typeof membershipService.getLeagueMemberships).toBe('function');
|
||||
});
|
||||
|
||||
it('resolves domain services without errors', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(DRIVER_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(TEAM_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(RACE_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(DASHBOARD_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const driverService = container.get(DRIVER_SERVICE_TOKEN);
|
||||
const teamService = container.get(TEAM_SERVICE_TOKEN);
|
||||
const raceService = container.get(RACE_SERVICE_TOKEN);
|
||||
const dashboardService = container.get(DASHBOARD_SERVICE_TOKEN);
|
||||
|
||||
expect(driverService).toBeDefined();
|
||||
expect(teamService).toBeDefined();
|
||||
expect(raceService).toBeDefined();
|
||||
expect(dashboardService).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves all services in a single operation', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
const tokens = [
|
||||
SESSION_SERVICE_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_SERVICE_TOKEN,
|
||||
LEAGUE_SERVICE_TOKEN,
|
||||
AUTH_SERVICE_TOKEN,
|
||||
DRIVER_SERVICE_TOKEN,
|
||||
TEAM_SERVICE_TOKEN,
|
||||
RACE_SERVICE_TOKEN,
|
||||
DASHBOARD_SERVICE_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
CONFIG_TOKEN,
|
||||
];
|
||||
|
||||
const services = tokens.map(token => {
|
||||
try {
|
||||
return container.get(token);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to resolve token ${token.toString()}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
expect(services.length).toBe(tokens.length);
|
||||
services.forEach((service) => {
|
||||
expect(service).toBeDefined();
|
||||
expect(service).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws clear error for non-existent bindings', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
const fakeToken = Symbol.for('Non.Existent.Service');
|
||||
expect(() => container.get(fakeToken)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws clear error for non-existent bindings', () => {
|
||||
const fakeToken = Symbol.for('Non.Existent.Service');
|
||||
|
||||
expect(() => container.get(fakeToken)).toThrow();
|
||||
describe('SSR Dynamic Environment Variables', () => {
|
||||
it('config binding is a function (not a string)', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container = createContainer();
|
||||
const config = container.get(CONFIG_TOKEN);
|
||||
|
||||
// Config should be a function that can be called
|
||||
expect(typeof config).toBe('function');
|
||||
|
||||
// Should be callable
|
||||
const configFn = config as ConfigFunction;
|
||||
expect(() => configFn()).not.toThrow();
|
||||
});
|
||||
|
||||
it('config function returns current environment value', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container = createContainer();
|
||||
const getConfig = container.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
const configValue = getConfig();
|
||||
// Should return some value (could be fallback in test env)
|
||||
expect(typeof configValue).toBe('string');
|
||||
expect(configValue.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('multiple containers share the same config behavior', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container1 = createContainer();
|
||||
const container2 = createContainer();
|
||||
|
||||
const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
// Both should be functions
|
||||
expect(typeof config1).toBe('function');
|
||||
expect(typeof config2).toBe('function');
|
||||
|
||||
// Both should return the same type of value
|
||||
expect(typeof config1()).toBe(typeof config2());
|
||||
});
|
||||
|
||||
it('ContainerManager singleton behavior', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const manager = ContainerManager.getInstance();
|
||||
const container1 = manager.getContainer();
|
||||
const container2 = manager.getContainer();
|
||||
|
||||
// Should be same instance
|
||||
expect(container1).toBe(container2);
|
||||
|
||||
const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
// Both should be functions
|
||||
expect(typeof config1).toBe('function');
|
||||
expect(typeof config2).toBe('function');
|
||||
});
|
||||
|
||||
it('API clients work with dynamic config', () => {
|
||||
process.env.API_BASE_URL = 'http://api-test:3001';
|
||||
|
||||
const container = createContainer();
|
||||
const leagueClient = container.get(LEAGUE_API_CLIENT_TOKEN);
|
||||
|
||||
expect(leagueClient).toBeDefined();
|
||||
expect(leagueClient).not.toBeNull();
|
||||
expect(typeof leagueClient).toBe('object');
|
||||
});
|
||||
|
||||
it('dispose clears singleton instance', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const manager = ContainerManager.getInstance();
|
||||
const container1 = manager.getContainer();
|
||||
|
||||
manager.dispose();
|
||||
|
||||
const container2 = manager.getContainer();
|
||||
|
||||
// Should be different instances after dispose
|
||||
expect(container1).not.toBe(container2);
|
||||
});
|
||||
|
||||
it('services work after container recreation', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container1 = createContainer();
|
||||
const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
ContainerManager.getInstance().dispose();
|
||||
|
||||
const container2 = createContainer();
|
||||
const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
// Both should be functions
|
||||
expect(typeof config1).toBe('function');
|
||||
expect(typeof config2).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user