mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
Add comprehensive setup and validation system for Umami - Create interactive setup wizard - Add setup validation script - Enhance error messages - Create SETUP.md documentation - Add .env.example template - Implement pre-flight checks - Add TypeScript types - Create tests - Update README
This commit is contained in:
parent
06422fb65f
commit
8ccaf3dcb0
15 changed files with 30895 additions and 7 deletions
207
scripts/__tests__/setup-integration.test.js
Normal file
207
scripts/__tests__/setup-integration.test.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import { execSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const projectRoot = join(__dirname, '../..');
|
||||
|
||||
describe('Setup Integration Tests', () => {
|
||||
describe('Complete Setup Flow', () => {
|
||||
it('should validate setup with all checks', () => {
|
||||
try {
|
||||
const output = execSync('node scripts/setup-validator.js', {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
expect(output).toContain('Validating Umami Setup');
|
||||
expect(output).toMatch(/Node\.js Version/);
|
||||
expect(output).toMatch(/Package Manager/);
|
||||
} catch (error) {
|
||||
// Test passes if script runs (even with failures)
|
||||
expect(error.stdout || error.stderr).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle missing .env file gracefully', () => {
|
||||
const envPath = join(projectRoot, '.env');
|
||||
const envExists = existsSync(envPath);
|
||||
|
||||
if (envExists) {
|
||||
// Skip if .env exists (don't want to break actual setup)
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
execSync('node scripts/setup-validator.js', {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
} catch (error) {
|
||||
const output = error.stdout || error.stderr || '';
|
||||
expect(output).toContain('Environment Configuration');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Recovery', () => {
|
||||
it('should provide helpful error messages', () => {
|
||||
try {
|
||||
execSync('node scripts/check-env.js', {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, DATABASE_URL: undefined },
|
||||
});
|
||||
} catch (error) {
|
||||
const output = error.stdout || '';
|
||||
// Should contain helpful information
|
||||
expect(output.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should exit with non-zero code on validation failure', () => {
|
||||
try {
|
||||
execSync('node scripts/check-env.js', {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, DATABASE_URL: undefined, SKIP_DB_CHECK: undefined },
|
||||
});
|
||||
// Should not reach here
|
||||
expect(false).toBe(true);
|
||||
} catch (error) {
|
||||
expect(error.status).not.toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Build Process Integration', () => {
|
||||
it('should have check-env in build script', () => {
|
||||
const packageJson = JSON.parse(
|
||||
execSync('cat package.json', {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.build).toContain('check-env');
|
||||
});
|
||||
|
||||
it('should have validate-setup script', () => {
|
||||
const packageJson = JSON.parse(
|
||||
execSync('cat package.json', {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts['validate-setup']).toBeDefined();
|
||||
expect(packageJson.scripts['validate-setup']).toContain('setup-validator');
|
||||
});
|
||||
|
||||
it('should have setup wizard script', () => {
|
||||
const packageJson = JSON.parse(
|
||||
execSync('cat package.json', {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.setup).toBeDefined();
|
||||
expect(packageJson.scripts.setup).toContain('quick-setup');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Script Execution', () => {
|
||||
it('should execute setup-validator without errors', () => {
|
||||
try {
|
||||
execSync('node scripts/setup-validator.js', {
|
||||
cwd: projectRoot,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
expect(true).toBe(true);
|
||||
} catch (error) {
|
||||
// Script may fail validation but should execute
|
||||
expect(error.status).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should execute check-env without syntax errors', () => {
|
||||
try {
|
||||
execSync('node scripts/check-env.js', {
|
||||
cwd: projectRoot,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
expect(true).toBe(true);
|
||||
} catch (error) {
|
||||
// Script may fail validation but should execute
|
||||
expect(error.status).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Documentation Files', () => {
|
||||
it('should have SETUP.md file', () => {
|
||||
const setupMdPath = join(projectRoot, 'SETUP.md');
|
||||
expect(existsSync(setupMdPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have .env.example file', () => {
|
||||
const envExamplePath = join(projectRoot, '.env.example');
|
||||
expect(existsSync(envExamplePath)).toBe(true);
|
||||
});
|
||||
|
||||
it('SETUP.md should contain key sections', () => {
|
||||
const setupMdPath = join(projectRoot, 'SETUP.md');
|
||||
if (existsSync(setupMdPath)) {
|
||||
const content = execSync(`cat "${setupMdPath}"`, {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
expect(content).toContain('Prerequisites');
|
||||
expect(content).toContain('Installation');
|
||||
expect(content).toContain('Database');
|
||||
expect(content).toContain('Troubleshooting');
|
||||
}
|
||||
});
|
||||
|
||||
it('.env.example should contain DATABASE_URL', () => {
|
||||
const envExamplePath = join(projectRoot, '.env.example');
|
||||
if (existsSync(envExamplePath)) {
|
||||
const content = execSync(`cat "${envExamplePath}"`, {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
expect(content).toContain('DATABASE_URL');
|
||||
expect(content).toContain('postgresql://');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation Scripts Exist', () => {
|
||||
const scripts = [
|
||||
'setup-validator.js',
|
||||
'quick-setup.js',
|
||||
'check-env.js',
|
||||
'check-db.js',
|
||||
'pre-dev.js',
|
||||
'pre-start.js',
|
||||
];
|
||||
|
||||
scripts.forEach(script => {
|
||||
it(`should have ${script}`, () => {
|
||||
const scriptPath = join(projectRoot, 'scripts', script);
|
||||
expect(existsSync(scriptPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
184
scripts/__tests__/setup-validator.test.js
Normal file
184
scripts/__tests__/setup-validator.test.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import {
|
||||
checkNodeVersion,
|
||||
checkPackageManager,
|
||||
checkEnvFile,
|
||||
checkDatabaseUrl,
|
||||
checkDependencies,
|
||||
} from '../setup-validator.js';
|
||||
|
||||
describe('Setup Validator', () => {
|
||||
describe('checkNodeVersion', () => {
|
||||
it('should pass when Node.js version is >= 18.18', async () => {
|
||||
const result = await checkNodeVersion();
|
||||
|
||||
expect(result.check).toBe('Node.js Version');
|
||||
expect(['pass', 'fail']).toContain(result.status);
|
||||
expect(result.message).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have proper structure', async () => {
|
||||
const result = await checkNodeVersion();
|
||||
|
||||
expect(result).toHaveProperty('check');
|
||||
expect(result).toHaveProperty('status');
|
||||
expect(result).toHaveProperty('message');
|
||||
|
||||
if (result.status === 'fail') {
|
||||
expect(result).toHaveProperty('solution');
|
||||
expect(result).toHaveProperty('documentation');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkPackageManager', () => {
|
||||
it('should check if pnpm is installed', async () => {
|
||||
const result = await checkPackageManager();
|
||||
|
||||
expect(result.check).toBe('Package Manager (pnpm)');
|
||||
expect(['pass', 'fail']).toContain(result.status);
|
||||
expect(result.message).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide solution when pnpm is not found', async () => {
|
||||
const result = await checkPackageManager();
|
||||
|
||||
if (result.status === 'fail') {
|
||||
expect(result.solution).toContain('pnpm');
|
||||
expect(result.documentation).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkEnvFile', () => {
|
||||
it('should check for .env file existence', async () => {
|
||||
const result = await checkEnvFile();
|
||||
|
||||
expect(result.check).toBe('Environment Configuration');
|
||||
expect(['pass', 'fail']).toContain(result.status);
|
||||
expect(result.message).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide solution when .env is missing', async () => {
|
||||
const result = await checkEnvFile();
|
||||
|
||||
if (result.status === 'fail') {
|
||||
expect(result.solution).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkDatabaseUrl', () => {
|
||||
const originalEnv = process.env.DATABASE_URL;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.DATABASE_URL = originalEnv;
|
||||
});
|
||||
|
||||
it('should fail when DATABASE_URL is not set', async () => {
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const result = await checkDatabaseUrl();
|
||||
|
||||
expect(result.check).toBe('Database URL Format');
|
||||
expect(result.status).toBe('fail');
|
||||
expect(result.message).toContain('DATABASE_URL');
|
||||
});
|
||||
|
||||
it('should fail when DATABASE_URL format is invalid', async () => {
|
||||
process.env.DATABASE_URL = 'invalid-url';
|
||||
|
||||
const result = await checkDatabaseUrl();
|
||||
|
||||
expect(result.status).toBe('fail');
|
||||
expect(result.message).toContain('format');
|
||||
expect(result.solution).toBeDefined();
|
||||
});
|
||||
|
||||
it('should pass when DATABASE_URL format is valid', async () => {
|
||||
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db';
|
||||
|
||||
const result = await checkDatabaseUrl();
|
||||
|
||||
expect(result.status).toBe('pass');
|
||||
expect(result.message).toContain('valid');
|
||||
});
|
||||
|
||||
it('should validate PostgreSQL URL pattern', async () => {
|
||||
const validUrls = [
|
||||
'postgresql://user:pass@localhost:5432/db',
|
||||
'postgresql://admin:secret@192.168.1.1:5432/umami',
|
||||
'postgresql://test:test123@db.example.com:5432/analytics',
|
||||
];
|
||||
|
||||
for (const url of validUrls) {
|
||||
process.env.DATABASE_URL = url;
|
||||
const result = await checkDatabaseUrl();
|
||||
expect(result.status).toBe('pass');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid URL patterns', async () => {
|
||||
const invalidUrls = [
|
||||
'mysql://user:pass@localhost:3306/db',
|
||||
'http://localhost:5432',
|
||||
'postgresql://localhost',
|
||||
'user:pass@localhost:5432/db',
|
||||
];
|
||||
|
||||
for (const url of invalidUrls) {
|
||||
process.env.DATABASE_URL = url;
|
||||
const result = await checkDatabaseUrl();
|
||||
expect(result.status).toBe('fail');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkDependencies', () => {
|
||||
it('should check for node_modules directory', async () => {
|
||||
const result = await checkDependencies();
|
||||
|
||||
expect(result.check).toBe('Dependencies');
|
||||
expect(['pass', 'fail']).toContain(result.status);
|
||||
expect(result.message).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide solution when dependencies are missing', async () => {
|
||||
const result = await checkDependencies();
|
||||
|
||||
if (result.status === 'fail') {
|
||||
expect(result.solution).toContain('pnpm install');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation Result Structure', () => {
|
||||
it('all checks should return consistent structure', async () => {
|
||||
const checks = [
|
||||
checkNodeVersion,
|
||||
checkPackageManager,
|
||||
checkEnvFile,
|
||||
checkDatabaseUrl,
|
||||
checkDependencies,
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
const result = await check();
|
||||
|
||||
expect(result).toHaveProperty('check');
|
||||
expect(result).toHaveProperty('status');
|
||||
expect(result).toHaveProperty('message');
|
||||
expect(typeof result.check).toBe('string');
|
||||
expect(['pass', 'fail', 'warning']).toContain(result.status);
|
||||
expect(typeof result.message).toBe('string');
|
||||
|
||||
if (result.solution) {
|
||||
expect(typeof result.solution).toBe('string');
|
||||
}
|
||||
|
||||
if (result.documentation) {
|
||||
expect(typeof result.documentation).toBe('string');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue