Single-file Node.js CLI that orchestrates the full setup lifecycle: - Interactive env var configuration with validation and smart defaults - Postgres provisioning via Docker Compose with readiness polling - Schema initialization (psql with docker exec fallback) - npm dependency installation with 120s timeout - Optional SQLite-to-Postgres data migration with retry logic - Frontend build with smart skip on reconfiguration Includes 84 tests: 50 property-based (fast-check) covering 19 correctness properties, and 34 integration tests for filesystem and parsing flows.
177 lines
6.7 KiB
JavaScript
177 lines
6.7 KiB
JavaScript
/**
|
|
* Property-Based Tests: Config Wizard Parsing Functions
|
|
*
|
|
* Feature: config-wizard
|
|
*
|
|
* Tests the parsing and derived-default functions from `configure.js`.
|
|
*
|
|
* Validates: Requirements 4.1, 4.2, 4.6
|
|
*/
|
|
|
|
const fc = require('fast-check');
|
|
const { resolveShellDefault, computeDerivedDefaults } = require('../../configure.js');
|
|
|
|
// --- Property 5: Shell variable default resolution ---
|
|
describe('Property 5: Shell variable default resolution', () => {
|
|
/**
|
|
* **Validates: Requirements 4.1**
|
|
*
|
|
* For any string containing the pattern ${VARNAME:-defaultvalue},
|
|
* resolveShellDefault extracts and returns defaultvalue.
|
|
* For any string not containing that pattern, it returns the original
|
|
* string (with surrounding quotes stripped).
|
|
*/
|
|
test('resolveShellDefault extracts default from ${VAR:-default} pattern', () => {
|
|
// Generate valid variable names and default values
|
|
const varNameArb = fc.stringMatching(/^[A-Z][A-Z0-9_]{0,19}$/);
|
|
const defaultValueArb = fc.string({ minLength: 1, maxLength: 50 })
|
|
.filter(s => !s.includes('}'));
|
|
|
|
fc.assert(
|
|
fc.property(varNameArb, defaultValueArb, (varName, defaultValue) => {
|
|
const input = `\${${varName}:-${defaultValue}}`;
|
|
const result = resolveShellDefault(input);
|
|
return result === defaultValue;
|
|
}),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
|
|
test('resolveShellDefault returns original string (quotes stripped) for non-matching patterns', () => {
|
|
// Generate strings that do NOT contain the ${VAR:-default} pattern
|
|
// and do not have leading/trailing quotes (which would be stripped)
|
|
const plainStringArb = fc.string({ minLength: 1, maxLength: 50 })
|
|
.filter(s =>
|
|
!/\$\{[^:}]+:-[^}]+\}/.test(s) &&
|
|
!s.startsWith("'") && !s.startsWith('"') &&
|
|
!s.endsWith("'") && !s.endsWith('"')
|
|
);
|
|
|
|
fc.assert(
|
|
fc.property(plainStringArb, (input) => {
|
|
const result = resolveShellDefault(input);
|
|
return result === input;
|
|
}),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
|
|
test('resolveShellDefault strips surrounding quotes from non-matching strings', () => {
|
|
const innerStringArb = fc.string({ minLength: 1, maxLength: 30 })
|
|
.filter(s => !s.includes("'") && !s.includes('"') && !/\$\{[^:}]+:-[^}]+\}/.test(s));
|
|
|
|
fc.assert(
|
|
fc.property(
|
|
innerStringArb,
|
|
fc.constantFrom("'", '"'),
|
|
(inner, quote) => {
|
|
const input = `${quote}${inner}${quote}`;
|
|
const result = resolveShellDefault(input);
|
|
return result === inner;
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
});
|
|
|
|
// --- Property 6: DATABASE_URL construction ---
|
|
describe('Property 6: DATABASE_URL construction', () => {
|
|
/**
|
|
* **Validates: Requirements 4.2**
|
|
*
|
|
* For any valid credentials tuple (user, password, port in [1,65535], database),
|
|
* the constructed URL equals postgresql://{user}:{password}@localhost:{port}/{database}.
|
|
*/
|
|
test('computeDerivedDefaults constructs correct DATABASE_URL from compose result', () => {
|
|
const credentialArb = fc.record({
|
|
user: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0 && !s.includes(':') && !s.includes('@') && !s.includes('/')),
|
|
password: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0 && !s.includes('@') && !s.includes('/')),
|
|
port: fc.integer({ min: 1, max: 65535 }).map(String),
|
|
database: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0 && !s.includes('/') && !s.includes('@') && !s.includes(':'))
|
|
});
|
|
|
|
fc.assert(
|
|
fc.property(credentialArb, (creds) => {
|
|
const result = computeDerivedDefaults('3001', 'localhost', creds);
|
|
const expected = `postgresql://${creds.user}:${creds.password}@localhost:${creds.port}/${creds.database}`;
|
|
return result.DATABASE_URL === expected;
|
|
}),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
|
|
test('computeDerivedDefaults sets databaseUrlSource to compose when compose result provided', () => {
|
|
const credentialArb = fc.record({
|
|
user: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
|
|
password: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
|
|
port: fc.integer({ min: 1, max: 65535 }).map(String),
|
|
database: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0)
|
|
});
|
|
|
|
fc.assert(
|
|
fc.property(credentialArb, (creds) => {
|
|
const result = computeDerivedDefaults('3001', 'localhost', creds);
|
|
return result.databaseUrlSource === 'compose';
|
|
}),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
});
|
|
|
|
// --- Property 7: Derived URL defaults from PORT and API_HOST ---
|
|
describe('Property 7: Derived URL defaults from PORT and API_HOST', () => {
|
|
/**
|
|
* **Validates: Requirements 4.6**
|
|
*
|
|
* For any valid port P and host H, REACT_APP_API_BASE equals
|
|
* http://{H}:{P}/api, REACT_APP_API_HOST equals http://{H}:{P},
|
|
* CORS_ORIGINS equals http://localhost:3000.
|
|
*/
|
|
test('derived defaults produce correct REACT_APP_API_BASE, REACT_APP_API_HOST, and CORS_ORIGINS', () => {
|
|
const portArb = fc.integer({ min: 1, max: 65535 }).map(String);
|
|
const hostArb = fc.string({ minLength: 1, maxLength: 50 })
|
|
.filter(s => s.trim().length > 0 && !s.includes(':') && !s.includes('/'));
|
|
|
|
fc.assert(
|
|
fc.property(portArb, hostArb, (port, host) => {
|
|
const result = computeDerivedDefaults(port, host, null);
|
|
|
|
const apiBaseCorrect = result.REACT_APP_API_BASE === `http://${host}:${port}/api`;
|
|
const apiHostCorrect = result.REACT_APP_API_HOST === `http://${host}:${port}`;
|
|
const corsCorrect = result.CORS_ORIGINS === 'http://localhost:3000';
|
|
|
|
return apiBaseCorrect && apiHostCorrect && corsCorrect;
|
|
}),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
|
|
test('CORS_ORIGINS is always http://localhost:3000 regardless of port and host', () => {
|
|
const portArb = fc.integer({ min: 1, max: 65535 }).map(String);
|
|
const hostArb = fc.string({ minLength: 1, maxLength: 30 })
|
|
.filter(s => s.trim().length > 0);
|
|
|
|
fc.assert(
|
|
fc.property(portArb, hostArb, (port, host) => {
|
|
const result = computeDerivedDefaults(port, host, null);
|
|
return result.CORS_ORIGINS === 'http://localhost:3000';
|
|
}),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
|
|
test('when composeResult is null, databaseUrlSource is fallback', () => {
|
|
const portArb = fc.integer({ min: 1, max: 65535 }).map(String);
|
|
const hostArb = fc.constantFrom('localhost', '0.0.0.0', '192.168.1.1');
|
|
|
|
fc.assert(
|
|
fc.property(portArb, hostArb, (port, host) => {
|
|
const result = computeDerivedDefaults(port, host, null);
|
|
return result.databaseUrlSource === 'fallback';
|
|
}),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
});
|