- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* GAMILIT MVP - Smoke Tests for Staging Environment
|
|
* ================================================
|
|
* Validates database seeds, backend APIs, and critical paths
|
|
* before production deployment.
|
|
*/
|
|
|
|
const { Pool } = require('pg');
|
|
const axios = require('axios');
|
|
|
|
// Configuration
|
|
const DB_CONFIG = {
|
|
connectionString: 'postgresql://gamilit_user:3RZ2uYhCnJBXQqEwPPbZK3NFfk4T4W4Q@localhost:5432/gamilit_platform'
|
|
};
|
|
|
|
const API_BASE_URL = 'http://localhost:3006';
|
|
const TEST_START_TIME = Date.now();
|
|
|
|
// Test Results Storage
|
|
const results = {
|
|
database: [],
|
|
backend: [],
|
|
critical: [],
|
|
warnings: []
|
|
};
|
|
|
|
// Color codes for console output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
green: '\x1b[32m',
|
|
red: '\x1b[31m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
cyan: '\x1b[36m'
|
|
};
|
|
|
|
function log(message, color = 'reset') {
|
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
}
|
|
|
|
function testResult(category, name, passed, details = '') {
|
|
const status = passed ? 'PASS' : 'FAIL';
|
|
const color = passed ? 'green' : 'red';
|
|
log(` [${status}] ${name}${details ? ' - ' + details : ''}`, color);
|
|
results[category].push({ name, passed, details });
|
|
return passed;
|
|
}
|
|
|
|
function warning(message) {
|
|
log(` [WARN] ${message}`, 'yellow');
|
|
results.warnings.push(message);
|
|
}
|
|
|
|
// ============================================================================
|
|
// DATABASE TESTS
|
|
// ============================================================================
|
|
|
|
async function testDatabaseConnectivity() {
|
|
log('\n=== 1. DATABASE CONNECTIVITY & SEEDS ===', 'cyan');
|
|
|
|
const pool = new Pool(DB_CONFIG);
|
|
|
|
try {
|
|
// Test 1.1: Database Connection
|
|
await pool.query('SELECT NOW()');
|
|
testResult('database', 'Database connection', true);
|
|
|
|
// Test 1.2: Verify modules exist
|
|
const modulesResult = await pool.query('SELECT COUNT(*) FROM educational_content.modules');
|
|
const moduleCount = parseInt(modulesResult.rows[0].count);
|
|
testResult('database', 'Modules table accessible', moduleCount > 0, `Found ${moduleCount} modules`);
|
|
|
|
if (moduleCount === 5) {
|
|
testResult('database', 'Expected module count (5)', true);
|
|
} else {
|
|
testResult('database', 'Expected module count (5)', false, `Found ${moduleCount} instead`);
|
|
}
|
|
|
|
// Test 1.3: Check modules 1-3 are published
|
|
const publishedModulesQuery = `
|
|
SELECT id, title, status, is_published
|
|
FROM educational_content.modules
|
|
WHERE id IN (1, 2, 3)
|
|
ORDER BY id
|
|
`;
|
|
const publishedModules = await pool.query(publishedModulesQuery);
|
|
|
|
let allPublished = true;
|
|
publishedModules.rows.forEach(module => {
|
|
const isCorrect = module.status === 'published' && module.is_published === true;
|
|
if (!isCorrect) {
|
|
allPublished = false;
|
|
testResult('database', `Module ${module.id} published status`, false,
|
|
`status=${module.status}, is_published=${module.is_published}`);
|
|
}
|
|
});
|
|
|
|
if (allPublished && publishedModules.rows.length === 3) {
|
|
testResult('database', 'Modules 1-3 published correctly', true);
|
|
}
|
|
|
|
// Test 1.4: Check modules 4-5 are in backlog
|
|
const backlogModulesQuery = `
|
|
SELECT id, title, status, is_published
|
|
FROM educational_content.modules
|
|
WHERE id IN (4, 5)
|
|
ORDER BY id
|
|
`;
|
|
const backlogModules = await pool.query(backlogModulesQuery);
|
|
|
|
let allBacklog = true;
|
|
backlogModules.rows.forEach(module => {
|
|
const isCorrect = module.status === 'backlog' && module.is_published === false;
|
|
if (!isCorrect) {
|
|
allBacklog = false;
|
|
testResult('database', `Module ${module.id} backlog status`, false,
|
|
`status=${module.status}, is_published=${module.is_published}`);
|
|
}
|
|
});
|
|
|
|
if (allBacklog && backlogModules.rows.length === 2) {
|
|
testResult('database', 'Modules 4-5 in backlog correctly', true);
|
|
}
|
|
|
|
// Test 1.5: Check exercises for modules 1-3
|
|
const exercisesQuery = `
|
|
SELECT module_id, COUNT(*) as count
|
|
FROM educational_content.exercises
|
|
WHERE module_id IN (1, 2, 3)
|
|
GROUP BY module_id
|
|
ORDER BY module_id
|
|
`;
|
|
const exercisesResult = await pool.query(exercisesQuery);
|
|
|
|
let totalExercises = 0;
|
|
exercisesResult.rows.forEach(row => {
|
|
totalExercises += parseInt(row.count);
|
|
log(` Module ${row.module_id}: ${row.count} exercises`, 'blue');
|
|
});
|
|
|
|
if (totalExercises >= 17) {
|
|
testResult('database', 'Exercise count for modules 1-3', true, `${totalExercises} exercises found (≥17 required)`);
|
|
} else {
|
|
testResult('database', 'Exercise count for modules 1-3', false, `Only ${totalExercises} exercises found (<17 required)`);
|
|
}
|
|
|
|
// Test 1.6: Check for orphaned records
|
|
const orphanedExercisesQuery = `
|
|
SELECT COUNT(*) FROM educational_content.exercises e
|
|
WHERE NOT EXISTS (SELECT 1 FROM educational_content.modules m WHERE m.id = e.module_id)
|
|
`;
|
|
const orphanedExercises = await pool.query(orphanedExercisesQuery);
|
|
const orphanedCount = parseInt(orphanedExercises.rows[0].count);
|
|
|
|
testResult('database', 'No orphaned exercises', orphanedCount === 0,
|
|
orphanedCount > 0 ? `Found ${orphanedCount} orphaned records` : '');
|
|
|
|
await pool.end();
|
|
return true;
|
|
|
|
} catch (error) {
|
|
testResult('database', 'Database connectivity', false, error.message);
|
|
await pool.end();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// BACKEND API TESTS
|
|
// ============================================================================
|
|
|
|
async function testBackendAPIs() {
|
|
log('\n=== 2. BACKEND API ENDPOINTS ===', 'cyan');
|
|
|
|
try {
|
|
// Test 2.1: GET /api/modules
|
|
try {
|
|
const modulesResponse = await axios.get(`${API_BASE_URL}/api/modules`);
|
|
const modules = modulesResponse.data;
|
|
|
|
if (Array.isArray(modules) && modules.length === 5) {
|
|
testResult('backend', 'GET /api/modules returns 5 modules', true);
|
|
} else {
|
|
testResult('backend', 'GET /api/modules returns 5 modules', false,
|
|
`Returned ${Array.isArray(modules) ? modules.length : 'non-array'} modules`);
|
|
}
|
|
|
|
// Check published modules
|
|
const publishedModules = modules.filter(m => m.is_published === true);
|
|
testResult('backend', 'Published modules count', publishedModules.length === 3,
|
|
`Found ${publishedModules.length} published modules`);
|
|
|
|
} catch (error) {
|
|
testResult('backend', 'GET /api/modules', false, error.message);
|
|
}
|
|
|
|
// Test 2.2: GET /api/modules/:id for modules 1-3 (should include exercises)
|
|
for (let moduleId of [1, 2, 3]) {
|
|
try {
|
|
const moduleResponse = await axios.get(`${API_BASE_URL}/api/modules/${moduleId}`);
|
|
const module = moduleResponse.data;
|
|
|
|
const hasExercises = module.exercises && Array.isArray(module.exercises) && module.exercises.length > 0;
|
|
testResult('backend', `GET /api/modules/${moduleId} includes exercises`, hasExercises,
|
|
hasExercises ? `${module.exercises.length} exercises` : 'No exercises found');
|
|
|
|
} catch (error) {
|
|
testResult('backend', `GET /api/modules/${moduleId}`, false, error.message);
|
|
}
|
|
}
|
|
|
|
// Test 2.3: GET /api/modules/:id for modules 4-5 (should show backlog)
|
|
for (let moduleId of [4, 5]) {
|
|
try {
|
|
const moduleResponse = await axios.get(`${API_BASE_URL}/api/modules/${moduleId}`);
|
|
const module = moduleResponse.data;
|
|
|
|
const isBacklog = module.status === 'backlog' && module.is_published === false;
|
|
testResult('backend', `GET /api/modules/${moduleId} shows backlog`, isBacklog);
|
|
|
|
} catch (error) {
|
|
testResult('backend', `GET /api/modules/${moduleId}`, false, error.message);
|
|
}
|
|
}
|
|
|
|
// Test 2.4: Gamification endpoints (if available)
|
|
try {
|
|
const healthResponse = await axios.get(`${API_BASE_URL}/api/health`);
|
|
testResult('backend', 'Backend health check', healthResponse.status === 200);
|
|
} catch (error) {
|
|
warning('Backend health endpoint not available or not responding');
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
testResult('backend', 'Backend API tests', false, error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CRITICAL USER FLOWS
|
|
// ============================================================================
|
|
|
|
async function testCriticalFlows() {
|
|
log('\n=== 3. CRITICAL USER FLOWS ===', 'cyan');
|
|
|
|
try {
|
|
// Test 3.1: Student can view published modules
|
|
const modulesResponse = await axios.get(`${API_BASE_URL}/api/modules`);
|
|
const publishedModules = modulesResponse.data.filter(m => m.is_published === true);
|
|
|
|
testResult('critical', 'Student can view published modules',
|
|
publishedModules.length >= 3, `${publishedModules.length} published modules available`);
|
|
|
|
// Test 3.2: Student can access module details with exercises
|
|
let canAccessExercises = true;
|
|
for (let module of publishedModules.slice(0, 3)) {
|
|
try {
|
|
const detailResponse = await axios.get(`${API_BASE_URL}/api/modules/${module.id}`);
|
|
if (!detailResponse.data.exercises || detailResponse.data.exercises.length === 0) {
|
|
canAccessExercises = false;
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
canAccessExercises = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
testResult('critical', 'Student can access exercises for published modules', canAccessExercises);
|
|
|
|
// Test 3.3: Backlog modules are properly marked
|
|
const backlogModules = modulesResponse.data.filter(m => m.status === 'backlog');
|
|
testResult('critical', 'Backlog modules properly identified',
|
|
backlogModules.length >= 2, `${backlogModules.length} modules in backlog`);
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
testResult('critical', 'Critical user flows', false, error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN EXECUTION
|
|
// ============================================================================
|
|
|
|
async function runSmokeTests() {
|
|
log('\n╔════════════════════════════════════════════════════════════════╗', 'cyan');
|
|
log('║ GAMILIT MVP - STAGING SMOKE TESTS ║', 'cyan');
|
|
log('║ Testing critical paths before production deploy ║', 'cyan');
|
|
log('╚════════════════════════════════════════════════════════════════╝', 'cyan');
|
|
|
|
// Run all test suites
|
|
await testDatabaseConnectivity();
|
|
await testBackendAPIs();
|
|
await testCriticalFlows();
|
|
|
|
// Generate summary
|
|
log('\n=== TEST SUMMARY ===', 'cyan');
|
|
|
|
const allTests = [
|
|
...results.database,
|
|
...results.backend,
|
|
...results.critical
|
|
];
|
|
|
|
const passed = allTests.filter(t => t.passed).length;
|
|
const failed = allTests.filter(t => !t.passed).length;
|
|
const total = allTests.length;
|
|
|
|
log(`\nTotal Tests: ${total}`);
|
|
log(`Passed: ${passed}`, 'green');
|
|
log(`Failed: ${failed}`, failed > 0 ? 'red' : 'green');
|
|
log(`Warnings: ${results.warnings.length}`, results.warnings.length > 0 ? 'yellow' : 'green');
|
|
|
|
const executionTime = ((Date.now() - TEST_START_TIME) / 1000).toFixed(2);
|
|
log(`\nExecution Time: ${executionTime}s`);
|
|
|
|
// Determine if production-ready
|
|
const criticalFailures = results.critical.filter(t => !t.passed).length;
|
|
const databaseFailures = results.database.filter(t => !t.passed).length;
|
|
|
|
log('\n=== RECOMMENDATION ===', 'cyan');
|
|
|
|
if (criticalFailures === 0 && databaseFailures === 0 && failed <= 2) {
|
|
log('✓ APPROVE - MVP is ready for production deployment', 'green');
|
|
log(' All critical tests passed. Any failures are non-blocking.', 'green');
|
|
} else if (criticalFailures > 0) {
|
|
log('✗ BLOCK - Critical failures detected', 'red');
|
|
log(` ${criticalFailures} critical test(s) failed. Must be resolved before deploy.`, 'red');
|
|
} else if (databaseFailures > 2) {
|
|
log('✗ BLOCK - Database integrity issues', 'red');
|
|
log(` ${databaseFailures} database test(s) failed. Review seed data.`, 'red');
|
|
} else {
|
|
log('⚠ CAUTION - Some tests failed, review required', 'yellow');
|
|
log(` ${failed} test(s) failed. Review before deployment.`, 'yellow');
|
|
}
|
|
|
|
// Return exit code
|
|
process.exit(criticalFailures > 0 || databaseFailures > 2 ? 1 : 0);
|
|
}
|
|
|
|
// Run tests
|
|
runSmokeTests().catch(error => {
|
|
log('\n✗ FATAL ERROR: Smoke tests failed to execute', 'red');
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|