Add unified setup script (configure.js) merging deploy + config wizard
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.
This commit is contained in:
176
backend/__tests__/config-wizard-parsing.property.test.js
Normal file
176
backend/__tests__/config-wizard-parsing.property.test.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user