487 lines
20 KiB
Markdown
487 lines
20 KiB
Markdown
|
|
# Design Document: Config Wizard
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
The Config Wizard is a single-file Node.js CLI script (`configure.js`) at the project root that interactively guides deployers through configuring all environment variables for the CVE Dashboard. It replaces the manual process of copying `.env.example` and editing values by hand.
|
||
|
|
|
||
|
|
The wizard uses only the Node.js built-in `readline` module — no external dependencies. It writes two output files: `backend/.env` and `frontend/.env`. It derives smart defaults by parsing `docker-compose.yml` for Postgres credentials and propagates the backend PORT into frontend URL defaults.
|
||
|
|
|
||
|
|
Key design goals:
|
||
|
|
- Zero dependencies beyond Node.js 18+ standard library
|
||
|
|
- Single file implementation for easy maintenance
|
||
|
|
- Idempotent re-runs that preserve existing values and unmanaged variables
|
||
|
|
- Group-based flow with skip capability for unused integrations
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
The wizard follows a linear pipeline architecture:
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
flowchart TD
|
||
|
|
A[Start: node configure.js] --> B[Validate project root]
|
||
|
|
B --> C[Parse existing .env files]
|
||
|
|
C --> D[Parse docker-compose.yml for defaults]
|
||
|
|
D --> E[Display welcome message]
|
||
|
|
E --> F[Group loop: prompt variables]
|
||
|
|
F --> G[Display summary]
|
||
|
|
G --> H{User confirms?}
|
||
|
|
H -->|Yes| I[Handle existing file backup/overwrite]
|
||
|
|
I --> J[Write backend/.env]
|
||
|
|
J --> K[Write frontend/.env]
|
||
|
|
K --> L[Display success + next steps]
|
||
|
|
H -->|No| M{Restart or exit?}
|
||
|
|
M -->|Restart| E
|
||
|
|
M -->|Exit| N[Exit code 1]
|
||
|
|
B -->|Missing dirs| O[Error + exit code 1]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Execution Flow
|
||
|
|
|
||
|
|
1. **Validation** — Confirm `backend/` and `frontend/` directories exist relative to CWD
|
||
|
|
2. **Parse existing** — Read current `backend/.env` and `frontend/.env` if they exist, extract key-value pairs
|
||
|
|
3. **Parse docker-compose** — Extract Postgres credentials from `docker-compose.yml` using line-by-line parsing
|
||
|
|
4. **Welcome** — Display purpose and instructions
|
||
|
|
5. **Group loop** — Iterate through variable groups in order, prompting for each variable
|
||
|
|
6. **Summary** — Display all configured values (sensitive ones masked) and target file paths
|
||
|
|
7. **Confirmation** — User approves or rejects; rejection offers restart or exit
|
||
|
|
8. **Write** — Generate env file content and write to disk with backup handling
|
||
|
|
|
||
|
|
### Signal Handling
|
||
|
|
|
||
|
|
A `SIGINT` handler (Ctrl+C) is registered at startup. When triggered:
|
||
|
|
- Close the readline interface
|
||
|
|
- Print a cancellation message
|
||
|
|
- Exit with code 1
|
||
|
|
- No files are written regardless of progress
|
||
|
|
|
||
|
|
## Components and Interfaces
|
||
|
|
|
||
|
|
### Module Structure
|
||
|
|
|
||
|
|
All code lives in a single `configure.js` file organized into these logical sections:
|
||
|
|
|
||
|
|
```
|
||
|
|
configure.js
|
||
|
|
├── Constants & Configuration
|
||
|
|
│ ├── VARIABLE_DESCRIPTORS[] — metadata for all managed variables
|
||
|
|
│ ├── GROUP_ORDER[] — ordered list of group names
|
||
|
|
│ ├── GROUP_DESCRIPTIONS{} — one-line description per group
|
||
|
|
│ └── SENSITIVE_VARS[] — list of variable names to mask
|
||
|
|
├── Parsing Functions
|
||
|
|
│ ├── parseEnvFile(filePath) — read existing .env into key-value map
|
||
|
|
│ ├── parseDockerCompose(filePath) — extract Postgres config
|
||
|
|
│ └── resolveShellDefault(str) — extract default from ${VAR:-default} syntax
|
||
|
|
├── Validation Functions
|
||
|
|
│ ├── validatePort(value)
|
||
|
|
│ ├── validateCorsOrigins(value)
|
||
|
|
│ ├── validateDatabaseUrl(value)
|
||
|
|
│ ├── validateSessionSecret(value)
|
||
|
|
│ └── validateRequired(value)
|
||
|
|
├── Display Functions
|
||
|
|
│ ├── printWelcome()
|
||
|
|
│ ├── printGroupHeader(group)
|
||
|
|
│ ├── printSummary(config, skippedGroups)
|
||
|
|
│ └── maskSensitive(name, value)
|
||
|
|
├── Prompt Functions
|
||
|
|
│ ├── promptVariable(rl, descriptor, currentValue)
|
||
|
|
│ ├── promptYesNo(rl, question, defaultNo)
|
||
|
|
│ └── promptOverwrite(rl, filePath)
|
||
|
|
├── File Writing
|
||
|
|
│ ├── generateEnvContent(variables, groups)
|
||
|
|
│ ├── writeEnvFile(filePath, content)
|
||
|
|
│ └── createBackup(filePath)
|
||
|
|
└── Main Flow
|
||
|
|
└── main()
|
||
|
|
```
|
||
|
|
|
||
|
|
### Key Interfaces
|
||
|
|
|
||
|
|
#### Variable Descriptor
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
{
|
||
|
|
name: String, // e.g. "PORT"
|
||
|
|
group: String, // e.g. "Core Settings"
|
||
|
|
required: Boolean, // true = must have a value
|
||
|
|
default: String|null, // factory default value
|
||
|
|
description: String, // max 120 chars
|
||
|
|
docUrl: String|null, // URL for obtaining the value
|
||
|
|
sensitive: Boolean, // true = mask in display
|
||
|
|
validator: String|null // name of validation function to apply
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Parsed Config State
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
{
|
||
|
|
values: Map<String, String>, // variable name → entered value
|
||
|
|
skippedGroups: Set<String>, // groups the user declined
|
||
|
|
existingBackend: Map<String, String>, // parsed from existing backend/.env
|
||
|
|
existingFrontend: Map<String, String>, // parsed from existing frontend/.env
|
||
|
|
unmanagedBackend: String[], // lines not matching managed vars
|
||
|
|
unmanagedFrontend: String[], // lines not matching managed vars
|
||
|
|
derivedDefaults: { // computed from docker-compose/port
|
||
|
|
DATABASE_URL: String|null,
|
||
|
|
databaseUrlSource: 'compose'|'fallback',
|
||
|
|
REACT_APP_API_BASE: String|null,
|
||
|
|
CORS_ORIGINS: String|null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### parseEnvFile(filePath) → { managed: Map, unmanaged: String[] }
|
||
|
|
|
||
|
|
Reads a `.env` file line by line. For each non-empty, non-comment line, splits on the first `=` character. If the key matches a managed variable, stores it in `managed`. Otherwise, preserves the raw line (including preceding comments) in `unmanaged`.
|
||
|
|
|
||
|
|
#### parseDockerCompose(filePath) → { user, password, database, port } | null
|
||
|
|
|
||
|
|
Line-by-line parser that extracts:
|
||
|
|
- `POSTGRES_USER` from `POSTGRES_USER: value` or `POSTGRES_USER: ${VAR:-default}`
|
||
|
|
- `POSTGRES_PASSWORD` from `POSTGRES_PASSWORD: ${VAR:-default}` (resolves the default)
|
||
|
|
- `POSTGRES_DB` from `POSTGRES_DB: value`
|
||
|
|
- Host port from `- "host:container"` under the `ports:` key
|
||
|
|
|
||
|
|
Returns `null` if the file doesn't exist or can't be parsed.
|
||
|
|
|
||
|
|
#### generateEnvContent(variables, groupOrder, groupDescriptions) → String
|
||
|
|
|
||
|
|
Produces the final `.env` file content:
|
||
|
|
- Group header comments (`# --- Group Name ---`)
|
||
|
|
- `KEY=value` lines, with values containing spaces/`#`/quotes wrapped in double quotes
|
||
|
|
- Omits optional variables with no value
|
||
|
|
- Appends unmanaged variables in a `# Custom Variables` section
|
||
|
|
|
||
|
|
## Data Models
|
||
|
|
|
||
|
|
### Variable Descriptor Registry
|
||
|
|
|
||
|
|
The complete list of managed variables with their metadata:
|
||
|
|
|
||
|
|
| Name | Group | Required | Default | Sensitive | Validator |
|
||
|
|
|------|-------|----------|---------|-----------|-----------|
|
||
|
|
| `PORT` | Core Settings | yes | `3001` | no | `validatePort` |
|
||
|
|
| `API_HOST` | Core Settings | yes | `localhost` | no | — |
|
||
|
|
| `CORS_ORIGINS` | Core Settings | yes | (derived) | no | `validateCorsOrigins` |
|
||
|
|
| `DATABASE_URL` | Database | yes | (derived) | yes | `validateDatabaseUrl` |
|
||
|
|
| `SESSION_SECRET` | Session | yes | — | yes | `validateSessionSecret` |
|
||
|
|
| `NVD_API_KEY` | NVD API | no | — | yes | — |
|
||
|
|
| `IVANTI_API_KEY` | Ivanti Integration | no | — | yes | — |
|
||
|
|
| `IVANTI_CLIENT_ID` | Ivanti Integration | no | `1550` | no | — |
|
||
|
|
| `IVANTI_FIRST_NAME` | Ivanti Integration | no | — | no | — |
|
||
|
|
| `IVANTI_LAST_NAME` | Ivanti Integration | no | — | no | — |
|
||
|
|
| `IVANTI_BU_FILTER` | Ivanti Integration | no | `NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM` | no | — |
|
||
|
|
| `IVANTI_MANAGED_BUS` | Ivanti Integration | no | `NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM` | no | — |
|
||
|
|
| `IVANTI_SKIP_TLS` | Ivanti Integration | no | `false` | no | — |
|
||
|
|
| `ATLAS_API_URL` | Atlas Integration | no | — | no | — |
|
||
|
|
| `ATLAS_API_USER` | Atlas Integration | no | — | no | — |
|
||
|
|
| `ATLAS_API_PASS` | Atlas Integration | no | — | yes | — |
|
||
|
|
| `ATLAS_SKIP_TLS` | Atlas Integration | no | `false` | no | — |
|
||
|
|
| `JIRA_BASE_URL` | Jira Integration | no | — | no | — |
|
||
|
|
| `JIRA_AUTH_METHOD` | Jira Integration | no | `basic` | no | — |
|
||
|
|
| `JIRA_API_USER` | Jira Integration | no | — | no | — |
|
||
|
|
| `JIRA_API_TOKEN` | Jira Integration | no | — | yes | — |
|
||
|
|
| `JIRA_PAT` | Jira Integration | no | — | yes | — |
|
||
|
|
| `JIRA_PROJECT_KEY` | Jira Integration | no | — | no | — |
|
||
|
|
| `JIRA_ISSUE_TYPE` | Jira Integration | no | `Task` | no | — |
|
||
|
|
| `JIRA_SKIP_TLS` | Jira Integration | no | `false` | no | — |
|
||
|
|
| `CARD_API_URL` | CARD Integration | no | — | no | — |
|
||
|
|
| `CARD_API_USER` | CARD Integration | no | — | no | — |
|
||
|
|
| `CARD_API_PASS` | CARD Integration | no | — | yes | — |
|
||
|
|
| `CARD_SKIP_TLS` | CARD Integration | no | `false` | no | — |
|
||
|
|
| `GITLAB_URL` | GitLab Integration | no | `http://steam-gitlab.charterlab.com` | no | — |
|
||
|
|
| `GITLAB_PROJECT_ID` | GitLab Integration | no | — | no | — |
|
||
|
|
| `GITLAB_PAT` | GitLab Integration | no | — | yes | — |
|
||
|
|
| `REACT_APP_API_BASE` | Frontend Settings | yes | (derived) | no | — |
|
||
|
|
| `REACT_APP_API_HOST` | Frontend Settings | yes | (derived) | no | — |
|
||
|
|
|
||
|
|
### Group Order and Descriptions
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
const GROUP_ORDER = [
|
||
|
|
'Core Settings',
|
||
|
|
'Database',
|
||
|
|
'Session',
|
||
|
|
'NVD API',
|
||
|
|
'Ivanti Integration',
|
||
|
|
'Atlas Integration',
|
||
|
|
'Jira Integration',
|
||
|
|
'CARD Integration',
|
||
|
|
'GitLab Integration',
|
||
|
|
'Frontend Settings'
|
||
|
|
];
|
||
|
|
|
||
|
|
const GROUP_DESCRIPTIONS = {
|
||
|
|
'Core Settings': 'Server port, hostname, and CORS configuration',
|
||
|
|
'Database': 'PostgreSQL connection string for persistent storage',
|
||
|
|
'Session': 'Secret key for signing session cookies',
|
||
|
|
'NVD API': 'National Vulnerability Database API key for CVE lookups',
|
||
|
|
'Ivanti Integration': 'RiskSense platform credentials for vulnerability sync',
|
||
|
|
'Atlas Integration': 'Atlas InfoSec API for action plan management',
|
||
|
|
'Jira Integration': 'Jira Data Center for ticket creation and tracking',
|
||
|
|
'CARD Integration': 'CARD asset ownership API for host lookups',
|
||
|
|
'GitLab Integration': 'GitLab API for feedback submission (bug reports)',
|
||
|
|
'Frontend Settings': 'React app API endpoint configuration'
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Optional (Skippable) Groups
|
||
|
|
|
||
|
|
Groups that present a skip prompt before entering: `NVD API`, `Ivanti Integration`, `Atlas Integration`, `Jira Integration`, `CARD Integration`, `GitLab Integration`.
|
||
|
|
|
||
|
|
Core Settings, Database, Session, and Frontend Settings are always prompted.
|
||
|
|
|
||
|
|
### Docker-Compose Parsing Approach
|
||
|
|
|
||
|
|
The parser reads `docker-compose.yml` line by line without a full YAML parser. It uses a simple state machine:
|
||
|
|
|
||
|
|
1. **Find service** — Look for a line matching `postgres:` (indented under `services:`)
|
||
|
|
2. **Find environment** — Within the postgres service block, look for `environment:`
|
||
|
|
3. **Extract vars** — Read `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` values
|
||
|
|
4. **Find ports** — Look for `ports:` within the postgres service block
|
||
|
|
5. **Extract host port** — Parse `- "host:container"` to get the host port (left side)
|
||
|
|
|
||
|
|
Shell variable substitution (`${VAR:-default}`) is resolved by extracting the default value after `:-`.
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
function resolveShellDefault(value) {
|
||
|
|
const match = value.match(/\$\{[^:}]+:-([^}]+)\}/);
|
||
|
|
return match ? match[1] : value;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
If parsing fails at any step, the function returns `null` and the caller falls back to the hardcoded default: `postgresql://steam:sV4xmC9xAUCFop0ypxMVS056QgPqGrX@localhost:5433/cve_dashboard`.
|
||
|
|
|
||
|
|
### Env File Output Format
|
||
|
|
|
||
|
|
```
|
||
|
|
# --- Core Settings ---
|
||
|
|
PORT=3001
|
||
|
|
API_HOST=localhost
|
||
|
|
CORS_ORIGINS=http://localhost:3000
|
||
|
|
|
||
|
|
# --- Database ---
|
||
|
|
DATABASE_URL=postgresql://steam:sV4xmC9xAUCFop0ypxMVS056QgPqGrX@localhost:5433/cve_dashboard
|
||
|
|
|
||
|
|
# --- Session ---
|
||
|
|
SESSION_SECRET=my-very-long-secret-key-here
|
||
|
|
|
||
|
|
# --- Custom Variables ---
|
||
|
|
MY_CUSTOM_VAR=preserved_value
|
||
|
|
```
|
||
|
|
|
||
|
|
Values containing spaces, `#`, or quote characters are wrapped in double quotes:
|
||
|
|
```
|
||
|
|
SOME_VAR="value with spaces"
|
||
|
|
ANOTHER_VAR="value#with#hashes"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Correctness Properties
|
||
|
|
|
||
|
|
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||
|
|
|
||
|
|
### Property 1: Descriptor registry invariants
|
||
|
|
|
||
|
|
*For any* variable descriptor in the registry, the variable name must appear exactly once across all groups, and the group description must be non-empty and at most 120 characters long.
|
||
|
|
|
||
|
|
**Validates: Requirements 2.3, 2.5**
|
||
|
|
|
||
|
|
### Property 2: Variable ordering within groups
|
||
|
|
|
||
|
|
*For any* Variable_Group, all required variables in that group must appear before all optional variables in the descriptor list ordering.
|
||
|
|
|
||
|
|
**Validates: Requirements 2.4**
|
||
|
|
|
||
|
|
### Property 3: Group presentation order
|
||
|
|
|
||
|
|
*For any* two consecutive variables in the descriptor list, the first variable's group index in GROUP_ORDER must be less than or equal to the second variable's group index.
|
||
|
|
|
||
|
|
**Validates: Requirements 2.1**
|
||
|
|
|
||
|
|
### Property 4: Sensitive value masking
|
||
|
|
|
||
|
|
*For any* string value longer than 8 characters, `maskSensitive` should return a string showing only the first 4 and last 4 characters with asterisks in between. *For any* string value of 8 characters or fewer, `maskSensitive` should return the full value unchanged.
|
||
|
|
|
||
|
|
**Validates: Requirements 3.4**
|
||
|
|
|
||
|
|
### Property 5: Shell variable default resolution
|
||
|
|
|
||
|
|
*For any* string containing the pattern `${VARNAME:-defaultvalue}`, `resolveShellDefault` should extract and return `defaultvalue`. *For any* string not containing that pattern, it should return the original string.
|
||
|
|
|
||
|
|
**Validates: Requirements 4.1**
|
||
|
|
|
||
|
|
### Property 6: DATABASE_URL construction
|
||
|
|
|
||
|
|
*For any* valid Postgres credentials tuple (user, password, port, database) where port is an integer in [1, 65535], the constructed DATABASE_URL should equal `postgresql://{user}:{password}@localhost:{port}/{database}`.
|
||
|
|
|
||
|
|
**Validates: Requirements 4.2**
|
||
|
|
|
||
|
|
### Property 7: Derived URL defaults from PORT
|
||
|
|
|
||
|
|
*For any* valid port number P, the derived `REACT_APP_API_BASE` should equal `http://localhost:{P}/api`, `REACT_APP_API_HOST` should equal `http://localhost:{P}`, and `CORS_ORIGINS` should equal `http://localhost:3000`.
|
||
|
|
|
||
|
|
**Validates: Requirements 4.6**
|
||
|
|
|
||
|
|
### Property 8: Port validation
|
||
|
|
|
||
|
|
*For any* string input, `validatePort` should return true if and only if the trimmed value is a string representation of an integer in the range [1, 65535].
|
||
|
|
|
||
|
|
**Validates: Requirements 5.2**
|
||
|
|
|
||
|
|
### Property 9: CORS origins validation
|
||
|
|
|
||
|
|
*For any* comma-separated string, `validateCorsOrigins` should return true if and only if every trimmed entry starts with `http://` or `https://`.
|
||
|
|
|
||
|
|
**Validates: Requirements 5.3**
|
||
|
|
|
||
|
|
### Property 10: DATABASE_URL validation
|
||
|
|
|
||
|
|
*For any* string, `validateDatabaseUrl` should return true if and only if the string starts with `postgresql://` or equals the literal string `sqlite`.
|
||
|
|
|
||
|
|
**Validates: Requirements 5.4**
|
||
|
|
|
||
|
|
### Property 11: SESSION_SECRET validation
|
||
|
|
|
||
|
|
*For any* string, `validateSessionSecret` should return true if and only if its length is at least 16 characters.
|
||
|
|
|
||
|
|
**Validates: Requirements 5.6**
|
||
|
|
|
||
|
|
### Property 12: Required variable rejection of whitespace
|
||
|
|
|
||
|
|
*For any* string composed entirely of whitespace characters (including empty string), `validateRequired` should return false.
|
||
|
|
|
||
|
|
**Validates: Requirements 5.1**
|
||
|
|
|
||
|
|
### Property 13: Env value quoting
|
||
|
|
|
||
|
|
*For any* key-value pair, `generateEnvContent` should wrap the value in double quotes if and only if the value contains a space, `#`, or quote character. Values without those characters should appear unquoted.
|
||
|
|
|
||
|
|
**Validates: Requirements 6.3**
|
||
|
|
|
||
|
|
### Property 14: Optional variable omission
|
||
|
|
|
||
|
|
*For any* optional variable descriptor with no user-provided value and no default value, `generateEnvContent` should not include a line for that variable in the output.
|
||
|
|
|
||
|
|
**Validates: Requirements 6.4**
|
||
|
|
|
||
|
|
### Property 15: Skipped group exclusion
|
||
|
|
|
||
|
|
*For any* Variable_Group that the user declines, the generated env file content should contain no `KEY=value` lines for any variable belonging to that group.
|
||
|
|
|
||
|
|
**Validates: Requirements 7.2, 7.3**
|
||
|
|
|
||
|
|
### Property 16: Env file round-trip parsing
|
||
|
|
|
||
|
|
*For any* valid env file content produced by `generateEnvContent`, parsing it with `parseEnvFile` should recover all the original key-value pairs for managed variables.
|
||
|
|
|
||
|
|
**Validates: Requirements 9.1, 9.2**
|
||
|
|
|
||
|
|
### Property 17: Unmanaged variable preservation
|
||
|
|
|
||
|
|
*For any* existing env file containing lines with keys not in the managed variable list, those lines should appear unchanged in the "Custom Variables" section of the generated output.
|
||
|
|
|
||
|
|
**Validates: Requirements 9.4**
|
||
|
|
|
||
|
|
### Property 18: Managed key deduplication
|
||
|
|
|
||
|
|
*For any* managed variable name that appears both in the wizard-entered values and in the unmanaged lines parsed from an existing file, the generated output should contain exactly one occurrence of that key, using the wizard-entered value.
|
||
|
|
|
||
|
|
**Validates: Requirements 9.5**
|
||
|
|
|
||
|
|
## Error Handling
|
||
|
|
|
||
|
|
### Categories
|
||
|
|
|
||
|
|
| Error Type | Handling Strategy |
|
||
|
|
|---|---|
|
||
|
|
| Missing project structure | Print error to stderr, exit code 1 |
|
||
|
|
| SIGINT (Ctrl+C) | Close readline, print cancellation, exit code 1, no files written |
|
||
|
|
| Invalid user input | Display format error, re-prompt same variable |
|
||
|
|
| Docker-compose parse failure | Log warning, fall back to hardcoded defaults |
|
||
|
|
| Existing env file unreadable | Log warning, use factory defaults, preserve file as backup |
|
||
|
|
| File write failure | Print error with path and reason, exit immediately without writing remaining files |
|
||
|
|
| Overwrite declined | Skip that file, continue with other files |
|
||
|
|
|
||
|
|
### Error Messages
|
||
|
|
|
||
|
|
All error messages follow the pattern: `Error: {what went wrong}. {what to do about it}.`
|
||
|
|
|
||
|
|
Examples:
|
||
|
|
- `Error: This script must be run from the project root (backend/ and frontend/ directories not found). Run from the directory containing both folders.`
|
||
|
|
- `Error: PORT must be an integer between 1 and 65535.`
|
||
|
|
- `Error: Could not write to backend/.env (Permission denied). Check file permissions and try again.`
|
||
|
|
|
||
|
|
### Graceful Degradation
|
||
|
|
|
||
|
|
The wizard degrades gracefully when optional infrastructure is missing:
|
||
|
|
- No `docker-compose.yml` → uses hardcoded DATABASE_URL default
|
||
|
|
- Malformed `docker-compose.yml` → warns and uses hardcoded default
|
||
|
|
- Existing `.env` unreadable → warns, backs up, uses factory defaults
|
||
|
|
- No existing `.env` files → normal first-run behavior
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Unit Tests
|
||
|
|
|
||
|
|
Unit tests cover the pure functions in isolation:
|
||
|
|
|
||
|
|
- `resolveShellDefault()` — various `${VAR:-default}` patterns
|
||
|
|
- `parseDockerCompose()` — valid compose files, missing files, malformed content
|
||
|
|
- `parseEnvFile()` — standard files, quoted values, comments, empty lines, malformed lines
|
||
|
|
- `validatePort()` — boundary values (0, 1, 65535, 65536), non-numeric, floats
|
||
|
|
- `validateCorsOrigins()` — single/multiple origins, invalid schemes, whitespace
|
||
|
|
- `validateDatabaseUrl()` — postgresql://, sqlite, invalid prefixes
|
||
|
|
- `validateSessionSecret()` — boundary at 15/16 characters
|
||
|
|
- `validateRequired()` — empty, whitespace-only, valid values
|
||
|
|
- `maskSensitive()` — short values (≤8), long values, exact boundary (8, 9 chars)
|
||
|
|
- `generateEnvContent()` — quoting rules, group headers, omission of empty optionals
|
||
|
|
|
||
|
|
### Property-Based Tests
|
||
|
|
|
||
|
|
Property-based tests use [fast-check](https://github.com/dubzzz/fast-check) to verify universal properties across generated inputs. Each property test runs a minimum of 100 iterations.
|
||
|
|
|
||
|
|
Tests are tagged with: `Feature: config-wizard, Property {N}: {title}`
|
||
|
|
|
||
|
|
Properties to implement:
|
||
|
|
1. Descriptor registry invariants (uniqueness, description length)
|
||
|
|
2. Variable ordering within groups (required before optional)
|
||
|
|
3. Group presentation order (monotonic group index)
|
||
|
|
4. Sensitive value masking (length-based behavior)
|
||
|
|
5. Shell variable default resolution (pattern extraction)
|
||
|
|
6. DATABASE_URL construction (format correctness)
|
||
|
|
7. Derived URL defaults from PORT (format correctness)
|
||
|
|
8. Port validation (range check)
|
||
|
|
9. CORS origins validation (scheme check)
|
||
|
|
10. DATABASE_URL validation (prefix check)
|
||
|
|
11. SESSION_SECRET validation (length check)
|
||
|
|
12. Required variable rejection of whitespace
|
||
|
|
13. Env value quoting (conditional wrapping)
|
||
|
|
14. Optional variable omission
|
||
|
|
15. Skipped group exclusion
|
||
|
|
16. Env file round-trip parsing
|
||
|
|
17. Unmanaged variable preservation
|
||
|
|
18. Managed key deduplication
|
||
|
|
|
||
|
|
### Integration Tests
|
||
|
|
|
||
|
|
Integration tests verify end-to-end flows using a temporary directory:
|
||
|
|
|
||
|
|
- Full wizard run with all defaults accepted → correct files written
|
||
|
|
- Wizard run with existing `.env` files → values pre-filled correctly
|
||
|
|
- Wizard run with skipped groups → those groups absent from output
|
||
|
|
- SIGINT at various points → no files written
|
||
|
|
- Missing project structure → error exit
|
||
|
|
- File write permission error → graceful failure
|
||
|
|
|
||
|
|
### Test Runner
|
||
|
|
|
||
|
|
Tests use Jest (already configured in the project via `react-scripts test` for frontend). Backend tests run with:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cd backend
|
||
|
|
npx jest __tests__/config-wizard*.test.js
|
||
|
|
```
|
||
|
|
|
||
|
|
Property tests use `fast-check` as the generator library within Jest test cases.
|
||
|
|
|