feature flags

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

View File

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