|
| 1 | +import fs from 'node:fs'; |
| 2 | +import path from 'node:path'; |
| 3 | +import { fileURLToPath } from 'node:url'; |
| 4 | +import { execSync } from 'node:child_process'; |
| 5 | + |
| 6 | +const __filename = fileURLToPath(import.meta.url); |
| 7 | +const __dirname = path.dirname(__filename); |
| 8 | +const rootDir = path.resolve(__dirname, '..'); |
| 9 | + |
| 10 | +console.log('🧪 Starting Infrastructure & Docker Test Suite...\n'); |
| 11 | + |
| 12 | +let errorCount = 0; |
| 13 | + |
| 14 | +function assert(condition, message) { |
| 15 | + if (!condition) { |
| 16 | + console.error(`❌ FAIL: ${message}`); |
| 17 | + errorCount++; |
| 18 | + } else { |
| 19 | + console.log(`✅ PASS: ${message}`); |
| 20 | + } |
| 21 | +} |
| 22 | + |
| 23 | +// ========================================== |
| 24 | +// 1. Makefile Verification |
| 25 | +// ========================================== |
| 26 | +console.log('📋 Testing Makefile...'); |
| 27 | +const makefilePath = path.join(rootDir, 'Makefile'); |
| 28 | +assert(fs.existsSync(makefilePath), 'Makefile exists'); |
| 29 | + |
| 30 | +if (fs.existsSync(makefilePath)) { |
| 31 | + const makefileContent = fs.readFileSync(makefilePath, 'utf-8'); |
| 32 | + const expectedTargets = [ |
| 33 | + 'all', |
| 34 | + 'help', |
| 35 | + 'install', |
| 36 | + 'start', |
| 37 | + 'build', |
| 38 | + 'serve', |
| 39 | + 'test', |
| 40 | + 'clean', |
| 41 | + 'docker-build', |
| 42 | + 'docker-buildx-builder', |
| 43 | + 'docker-buildx', |
| 44 | + 'docker-buildx-push', |
| 45 | + 'docker-run' |
| 46 | + ]; |
| 47 | + |
| 48 | + for (const target of expectedTargets) { |
| 49 | + const hasTarget = new RegExp(`^${target}:`, 'm').test(makefileContent); |
| 50 | + assert(hasTarget, `Makefile defines target '${target}'`); |
| 51 | + } |
| 52 | + |
| 53 | + assert(makefileContent.includes('docker/Dockerfile'), 'Makefile references docker/Dockerfile'); |
| 54 | + |
| 55 | + // Verify Makefile dry-run execution |
| 56 | + try { |
| 57 | + execSync('make -n test build docker-build docker-buildx', { |
| 58 | + cwd: rootDir, |
| 59 | + encoding: 'utf-8', |
| 60 | + stdio: 'pipe', |
| 61 | + }); |
| 62 | + assert(true, 'Makefile dry-run (make -n) executes with valid syntax'); |
| 63 | + } catch (err) { |
| 64 | + assert(false, `Makefile dry-run failed: ${err.message}`); |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +// ========================================== |
| 69 | +// 2. Docker Directory & Dockerfile Verification |
| 70 | +// ========================================== |
| 71 | +console.log('\n🐳 Testing Docker configs in docker/ directory...'); |
| 72 | +const dockerDir = path.join(rootDir, 'docker'); |
| 73 | +assert(fs.existsSync(dockerDir) && fs.statSync(dockerDir).isDirectory(), 'docker/ directory exists'); |
| 74 | + |
| 75 | +const dockerfilePath = path.join(dockerDir, 'Dockerfile'); |
| 76 | +assert(fs.existsSync(dockerfilePath), 'docker/Dockerfile exists'); |
| 77 | + |
| 78 | +if (fs.existsSync(dockerfilePath)) { |
| 79 | + const dockerContent = fs.readFileSync(dockerfilePath, 'utf-8'); |
| 80 | + |
| 81 | + assert(dockerContent.includes('FROM --platform=$BUILDPLATFORM node:22-alpine AS builder') || dockerContent.includes('AS builder'), 'docker/Dockerfile contains builder stage'); |
| 82 | + assert(dockerContent.includes('FROM nginx:alpine AS runner') || dockerContent.includes('FROM nginx:alpine'), 'docker/Dockerfile contains Nginx runner stage'); |
| 83 | + assert(dockerContent.includes('npm run build'), 'docker/Dockerfile runs static build step'); |
| 84 | + assert(dockerContent.includes('COPY docker/nginx.conf /etc/nginx/conf.d/default.conf'), 'docker/Dockerfile copies docker/nginx.conf to nginx conf.d'); |
| 85 | + assert(dockerContent.includes('COPY --from=builder /app/build /usr/share/nginx/html'), 'docker/Dockerfile copies build artifacts to web root'); |
| 86 | + assert(dockerContent.includes('EXPOSE 80'), 'docker/Dockerfile exposes port 80'); |
| 87 | + assert(dockerContent.includes('HEALTHCHECK'), 'docker/Dockerfile defines container healthcheck'); |
| 88 | +} |
| 89 | + |
| 90 | +// ========================================== |
| 91 | +// 3. .dockerignore Verification |
| 92 | +// ========================================== |
| 93 | +console.log('\n🚫 Testing .dockerignore...'); |
| 94 | +const dockerignoreInDocker = path.join(dockerDir, '.dockerignore'); |
| 95 | +const dockerignoreInRoot = path.join(rootDir, '.dockerignore'); |
| 96 | +assert(fs.existsSync(dockerignoreInDocker), 'docker/.dockerignore exists'); |
| 97 | +assert(fs.existsSync(dockerignoreInRoot), '.dockerignore exists at root'); |
| 98 | + |
| 99 | +if (fs.existsSync(dockerignoreInDocker)) { |
| 100 | + const ignoreContent = fs.readFileSync(dockerignoreInDocker, 'utf-8'); |
| 101 | + const requiredIgnores = ['node_modules', 'build', '.docusaurus', '.git']; |
| 102 | + for (const item of requiredIgnores) { |
| 103 | + assert(ignoreContent.includes(item), `docker/.dockerignore excludes '${item}'`); |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +// ========================================== |
| 108 | +// 4. Nginx Configuration Verification |
| 109 | +// ========================================== |
| 110 | +console.log('\n🌐 Testing docker/nginx.conf...'); |
| 111 | +const nginxPath = path.join(dockerDir, 'nginx.conf'); |
| 112 | +assert(fs.existsSync(nginxPath), 'docker/nginx.conf exists'); |
| 113 | + |
| 114 | +if (fs.existsSync(nginxPath)) { |
| 115 | + const nginxContent = fs.readFileSync(nginxPath, 'utf-8'); |
| 116 | + assert(nginxContent.includes('listen 80;'), 'docker/nginx.conf listens on port 80'); |
| 117 | + assert(nginxContent.includes('try_files'), 'docker/nginx.conf configures try_files for SPA routes'); |
| 118 | + assert(nginxContent.includes('gzip on;'), 'docker/nginx.conf enables gzip compression'); |
| 119 | +} |
| 120 | + |
| 121 | +// ========================================== |
| 122 | +// 5. GitHub Actions Workflow Verification |
| 123 | +// ========================================== |
| 124 | +console.log('\n⚙️ Testing GitHub Actions Workflow...'); |
| 125 | +const workflowPath = path.join(rootDir, '.github', 'workflows', 'ci.yml'); |
| 126 | +assert(fs.existsSync(workflowPath), '.github/workflows/ci.yml exists'); |
| 127 | + |
| 128 | +if (fs.existsSync(workflowPath)) { |
| 129 | + const workflowContent = fs.readFileSync(workflowPath, 'utf-8'); |
| 130 | + assert(workflowContent.includes('file: ./docker/Dockerfile'), 'CI workflow specifies file: ./docker/Dockerfile'); |
| 131 | + assert(workflowContent.includes('platforms: linux/amd64,linux/arm64'), 'CI workflow specifies multi-platform: linux/amd64,linux/arm64'); |
| 132 | + assert(workflowContent.includes('docker/setup-qemu-action'), 'CI workflow sets up QEMU'); |
| 133 | + assert(workflowContent.includes('docker/setup-buildx-action'), 'CI workflow sets up Docker Buildx'); |
| 134 | + assert(workflowContent.includes('docker/build-push-action'), 'CI workflow uses docker/build-push-action'); |
| 135 | + assert(workflowContent.includes('ghcr.io'), 'CI workflow targets GitHub Container Registry'); |
| 136 | +} |
| 137 | + |
| 138 | +console.log('\n========================================'); |
| 139 | +if (errorCount === 0) { |
| 140 | + console.log('🎉 ALL INFRASTRUCTURE TESTS PASSED CLEANLY!\n'); |
| 141 | + process.exit(0); |
| 142 | +} else { |
| 143 | + console.error(`💥 ${errorCount} infrastructure test(s) failed.\n`); |
| 144 | + process.exit(1); |
| 145 | +} |
0 commit comments