contract tests
This commit is contained in:
313
tests/contracts/bootstrap-contract.test.ts
Normal file
313
tests/contracts/bootstrap-contract.test.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user