314 lines
14 KiB
TypeScript
314 lines
14 KiB
TypeScript
/**
|
|
* Contract Validation Tests for Bootstrap Module
|
|
*
|
|
* These tests validate that the bootstrap module is properly configured and that
|
|
* the initialization process follows expected patterns. The bootstrap module is
|
|
* an internal initialization module that runs during application startup and
|
|
* does not expose HTTP endpoints.
|
|
*
|
|
* Key Findings:
|
|
* - Bootstrap module is an internal initialization module (not an API endpoint)
|
|
* - It runs during application startup via OnModuleInit lifecycle hook
|
|
* - It seeds the database with initial data (admin users, achievements, racing data)
|
|
* - It does not expose any HTTP controllers or endpoints
|
|
* - No API client exists in the website app for bootstrap operations
|
|
* - No bootstrap-related endpoints are defined in the OpenAPI spec
|
|
*/
|
|
|
|
import * as fs from 'fs/promises';
|
|
import * as path from 'path';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
interface OpenAPISpec {
|
|
openapi: string;
|
|
info: {
|
|
title: string;
|
|
description: string;
|
|
version: string;
|
|
};
|
|
paths: Record<string, any>;
|
|
components: {
|
|
schemas: Record<string, any>;
|
|
};
|
|
}
|
|
|
|
describe('Bootstrap Module Contract Validation', () => {
|
|
const apiRoot = path.join(__dirname, '../..');
|
|
const openapiPath = path.join(apiRoot, 'apps/api/openapi.json');
|
|
const bootstrapModulePath = path.join(apiRoot, 'apps/api/src/domain/bootstrap/BootstrapModule.ts');
|
|
const bootstrapAdaptersPath = path.join(apiRoot, 'adapters/bootstrap');
|
|
|
|
describe('OpenAPI Spec Integrity for Bootstrap', () => {
|
|
it('should NOT have bootstrap endpoints defined in OpenAPI spec', async () => {
|
|
const content = await fs.readFile(openapiPath, 'utf-8');
|
|
const spec: OpenAPISpec = JSON.parse(content);
|
|
|
|
// Bootstrap is an internal module, not an API endpoint
|
|
// Verify no bootstrap-related paths exist
|
|
const bootstrapPaths = Object.keys(spec.paths).filter(p => p.includes('bootstrap'));
|
|
expect(bootstrapPaths.length).toBe(0);
|
|
});
|
|
|
|
it('should NOT have bootstrap-related DTOs in OpenAPI spec', async () => {
|
|
const content = await fs.readFile(openapiPath, 'utf-8');
|
|
const spec: OpenAPISpec = JSON.parse(content);
|
|
|
|
// Bootstrap module doesn't expose DTOs for API consumption
|
|
// It uses internal DTOs for seeding data
|
|
const bootstrapSchemas = Object.keys(spec.components.schemas).filter(s =>
|
|
s.toLowerCase().includes('bootstrap') ||
|
|
s.toLowerCase().includes('seed')
|
|
);
|
|
expect(bootstrapSchemas.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Module Structure', () => {
|
|
it('should have BootstrapModule defined', async () => {
|
|
const bootstrapModuleExists = await fs.access(bootstrapModulePath).then(() => true).catch(() => false);
|
|
expect(bootstrapModuleExists).toBe(true);
|
|
});
|
|
|
|
it('should have BootstrapModule implement OnModuleInit', async () => {
|
|
const bootstrapModuleContent = await fs.readFile(bootstrapModulePath, 'utf-8');
|
|
|
|
// Verify it implements OnModuleInit lifecycle hook
|
|
expect(bootstrapModuleContent).toContain('implements OnModuleInit');
|
|
expect(bootstrapModuleContent).toContain('async onModuleInit()');
|
|
});
|
|
|
|
it('should have BootstrapModule with proper dependencies', async () => {
|
|
const bootstrapModuleContent = await fs.readFile(bootstrapModulePath, 'utf-8');
|
|
|
|
// Verify required dependencies are injected
|
|
expect(bootstrapModuleContent).toContain('@Inject(ENSURE_INITIAL_DATA_TOKEN)');
|
|
expect(bootstrapModuleContent).toContain('@Inject(SEED_DEMO_USERS_TOKEN)');
|
|
expect(bootstrapModuleContent).toContain('@Inject(\'Logger\')');
|
|
expect(bootstrapModuleContent).toContain('@Inject(\'RacingSeedDependencies\')');
|
|
});
|
|
|
|
it('should have BootstrapModule with proper imports', async () => {
|
|
const bootstrapModuleContent = await fs.readFile(bootstrapModulePath, 'utf-8');
|
|
|
|
// Verify persistence modules are imported
|
|
expect(bootstrapModuleContent).toContain('RacingPersistenceModule');
|
|
expect(bootstrapModuleContent).toContain('SocialPersistenceModule');
|
|
expect(bootstrapModuleContent).toContain('AchievementPersistenceModule');
|
|
expect(bootstrapModuleContent).toContain('IdentityPersistenceModule');
|
|
expect(bootstrapModuleContent).toContain('AdminPersistenceModule');
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Adapters Structure', () => {
|
|
it('should have EnsureInitialData adapter', async () => {
|
|
const ensureInitialDataPath = path.join(bootstrapAdaptersPath, 'EnsureInitialData.ts');
|
|
const ensureInitialDataExists = await fs.access(ensureInitialDataPath).then(() => true).catch(() => false);
|
|
expect(ensureInitialDataExists).toBe(true);
|
|
});
|
|
|
|
it('should have SeedDemoUsers adapter', async () => {
|
|
const seedDemoUsersPath = path.join(bootstrapAdaptersPath, 'SeedDemoUsers.ts');
|
|
const seedDemoUsersExists = await fs.access(seedDemoUsersPath).then(() => true).catch(() => false);
|
|
expect(seedDemoUsersExists).toBe(true);
|
|
});
|
|
|
|
it('should have SeedRacingData adapter', async () => {
|
|
const seedRacingDataPath = path.join(bootstrapAdaptersPath, 'SeedRacingData.ts');
|
|
const seedRacingDataExists = await fs.access(seedRacingDataPath).then(() => true).catch(() => false);
|
|
expect(seedRacingDataExists).toBe(true);
|
|
});
|
|
|
|
it('should have racing seed factories', async () => {
|
|
const racingDir = path.join(bootstrapAdaptersPath, 'racing');
|
|
const racingDirExists = await fs.access(racingDir).then(() => true).catch(() => false);
|
|
expect(racingDirExists).toBe(true);
|
|
|
|
// Verify key factory files exist
|
|
const racingFiles = await fs.readdir(racingDir);
|
|
expect(racingFiles).toContain('RacingDriverFactory.ts');
|
|
expect(racingFiles).toContain('RacingTeamFactory.ts');
|
|
expect(racingFiles).toContain('RacingLeagueFactory.ts');
|
|
expect(racingFiles).toContain('RacingRaceFactory.ts');
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Configuration', () => {
|
|
it('should have bootstrap configuration in environment', async () => {
|
|
const envPath = path.join(apiRoot, 'apps/api/src/env.ts');
|
|
const envContent = await fs.readFile(envPath, 'utf-8');
|
|
|
|
// Verify bootstrap configuration functions exist
|
|
expect(envContent).toContain('getEnableBootstrap');
|
|
expect(envContent).toContain('getForceReseed');
|
|
});
|
|
|
|
it('should have bootstrap enabled by default', async () => {
|
|
const envPath = path.join(apiRoot, 'apps/api/src/env.ts');
|
|
const envContent = await fs.readFile(envPath, 'utf-8');
|
|
|
|
// Verify bootstrap is enabled by default (for dev/test)
|
|
expect(envContent).toContain('GRIDPILOT_API_BOOTSTRAP');
|
|
expect(envContent).toContain('true'); // Default value
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Initialization Logic', () => {
|
|
it('should have proper initialization sequence', async () => {
|
|
const bootstrapModuleContent = await fs.readFile(bootstrapModulePath, 'utf-8');
|
|
|
|
// Verify initialization sequence
|
|
expect(bootstrapModuleContent).toContain('await this.ensureInitialData.execute()');
|
|
expect(bootstrapModuleContent).toContain('await this.shouldSeedRacingData()');
|
|
expect(bootstrapModuleContent).toContain('await this.shouldSeedDemoUsers()');
|
|
});
|
|
|
|
it('should have environment-aware seeding logic', async () => {
|
|
const bootstrapModuleContent = await fs.readFile(bootstrapModulePath, 'utf-8');
|
|
|
|
// Verify environment checks
|
|
expect(bootstrapModuleContent).toContain('process.env.NODE_ENV');
|
|
expect(bootstrapModuleContent).toContain('production');
|
|
expect(bootstrapModuleContent).toContain('inmemory');
|
|
expect(bootstrapModuleContent).toContain('postgres');
|
|
});
|
|
|
|
it('should have force reseed capability', async () => {
|
|
const bootstrapModuleContent = await fs.readFile(bootstrapModulePath, 'utf-8');
|
|
|
|
// Verify force reseed logic
|
|
expect(bootstrapModuleContent).toContain('getForceReseed()');
|
|
expect(bootstrapModuleContent).toContain('Force reseed enabled');
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Data Seeding', () => {
|
|
it('should seed initial admin user', async () => {
|
|
const ensureInitialDataPath = path.join(bootstrapAdaptersPath, 'EnsureInitialData.ts');
|
|
const ensureInitialDataContent = await fs.readFile(ensureInitialDataPath, 'utf-8');
|
|
|
|
// Verify admin user seeding
|
|
expect(ensureInitialDataContent).toContain('admin@gridpilot.local');
|
|
expect(ensureInitialDataContent).toContain('Admin');
|
|
expect(ensureInitialDataContent).toContain('signupUseCase');
|
|
});
|
|
|
|
it('should seed achievements', async () => {
|
|
const ensureInitialDataPath = path.join(bootstrapAdaptersPath, 'EnsureInitialData.ts');
|
|
const ensureInitialDataContent = await fs.readFile(ensureInitialDataPath, 'utf-8');
|
|
|
|
// Verify achievement seeding
|
|
expect(ensureInitialDataContent).toContain('DRIVER_ACHIEVEMENTS');
|
|
expect(ensureInitialDataContent).toContain('STEWARD_ACHIEVEMENTS');
|
|
expect(ensureInitialDataContent).toContain('ADMIN_ACHIEVEMENTS');
|
|
expect(ensureInitialDataContent).toContain('COMMUNITY_ACHIEVEMENTS');
|
|
expect(ensureInitialDataContent).toContain('createAchievementUseCase');
|
|
});
|
|
|
|
it('should seed demo users', async () => {
|
|
const seedDemoUsersPath = path.join(bootstrapAdaptersPath, 'SeedDemoUsers.ts');
|
|
const seedDemoUsersContent = await fs.readFile(seedDemoUsersPath, 'utf-8');
|
|
|
|
// Verify demo user seeding
|
|
expect(seedDemoUsersContent).toContain('SeedDemoUsers');
|
|
expect(seedDemoUsersContent).toContain('execute');
|
|
});
|
|
|
|
it('should seed racing data', async () => {
|
|
const seedRacingDataPath = path.join(bootstrapAdaptersPath, 'SeedRacingData.ts');
|
|
const seedRacingDataContent = await fs.readFile(seedRacingDataPath, 'utf-8');
|
|
|
|
// Verify racing data seeding
|
|
expect(seedRacingDataContent).toContain('SeedRacingData');
|
|
expect(seedRacingDataContent).toContain('execute');
|
|
expect(seedRacingDataContent).toContain('RacingSeedDependencies');
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Providers', () => {
|
|
it('should have BootstrapProviders defined', async () => {
|
|
const bootstrapProvidersPath = path.join(apiRoot, 'apps/api/src/domain/bootstrap/BootstrapProviders.ts');
|
|
const bootstrapProvidersExists = await fs.access(bootstrapProvidersPath).then(() => true).catch(() => false);
|
|
expect(bootstrapProvidersExists).toBe(true);
|
|
});
|
|
|
|
it('should have proper provider tokens', async () => {
|
|
const bootstrapProvidersContent = await fs.readFile(
|
|
path.join(apiRoot, 'apps/api/src/domain/bootstrap/BootstrapProviders.ts'),
|
|
'utf-8'
|
|
);
|
|
|
|
// Verify provider tokens are defined
|
|
expect(bootstrapProvidersContent).toContain('ENSURE_INITIAL_DATA_TOKEN');
|
|
expect(bootstrapProvidersContent).toContain('SEED_DEMO_USERS_TOKEN');
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Module Integration', () => {
|
|
it('should be imported in main app module', async () => {
|
|
const appModulePath = path.join(apiRoot, 'apps/api/src/app.module.ts');
|
|
const appModuleContent = await fs.readFile(appModulePath, 'utf-8');
|
|
|
|
// Verify BootstrapModule is imported
|
|
expect(appModuleContent).toContain('BootstrapModule');
|
|
expect(appModuleContent).toContain('./domain/bootstrap/BootstrapModule');
|
|
});
|
|
|
|
it('should be included in app module imports', async () => {
|
|
const appModulePath = path.join(apiRoot, 'apps/api/src/app.module.ts');
|
|
const appModuleContent = await fs.readFile(appModulePath, 'utf-8');
|
|
|
|
// Verify BootstrapModule is in imports array
|
|
expect(appModuleContent).toMatch(/imports:\s*\[[^\]]*BootstrapModule[^\]]*\]/s);
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Module Tests', () => {
|
|
it('should have unit tests for BootstrapModule', async () => {
|
|
const bootstrapModuleTestPath = path.join(apiRoot, 'apps/api/src/domain/bootstrap/BootstrapModule.test.ts');
|
|
const bootstrapModuleTestExists = await fs.access(bootstrapModuleTestPath).then(() => true).catch(() => false);
|
|
expect(bootstrapModuleTestExists).toBe(true);
|
|
});
|
|
|
|
it('should have postgres seed tests', async () => {
|
|
const postgresSeedTestPath = path.join(apiRoot, 'apps/api/src/domain/bootstrap/BootstrapModule.postgres-seed.test.ts');
|
|
const postgresSeedTestExists = await fs.access(postgresSeedTestPath).then(() => true).catch(() => false);
|
|
expect(postgresSeedTestExists).toBe(true);
|
|
});
|
|
|
|
it('should have racing seed tests', async () => {
|
|
const racingSeedTestPath = path.join(apiRoot, 'apps/api/src/domain/bootstrap/RacingSeed.test.ts');
|
|
const racingSeedTestExists = await fs.access(racingSeedTestPath).then(() => true).catch(() => false);
|
|
expect(racingSeedTestExists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Bootstrap Module Contract Summary', () => {
|
|
it('should document that bootstrap is an internal module', async () => {
|
|
const bootstrapModuleContent = await fs.readFile(bootstrapModulePath, 'utf-8');
|
|
|
|
// Verify bootstrap is documented as internal initialization
|
|
expect(bootstrapModuleContent).toContain('Initializing application data');
|
|
expect(bootstrapModuleContent).toContain('Bootstrap disabled');
|
|
});
|
|
|
|
it('should have no API client in website app', async () => {
|
|
const websiteApiDir = path.join(apiRoot, 'apps/website/lib/api');
|
|
const apiFiles = await fs.readdir(websiteApiDir);
|
|
|
|
// Verify no bootstrap API client exists
|
|
const bootstrapFiles = apiFiles.filter(f => f.toLowerCase().includes('bootstrap'));
|
|
expect(bootstrapFiles.length).toBe(0);
|
|
});
|
|
|
|
it('should have no bootstrap endpoints in OpenAPI', async () => {
|
|
const content = await fs.readFile(openapiPath, 'utf-8');
|
|
const spec: OpenAPISpec = JSON.parse(content);
|
|
|
|
// Verify no bootstrap paths exist
|
|
const allPaths = Object.keys(spec.paths);
|
|
const bootstrapPaths = allPaths.filter(p => p.toLowerCase().includes('bootstrap'));
|
|
expect(bootstrapPaths.length).toBe(0);
|
|
});
|
|
});
|
|
});
|