diff --git a/README.md b/README.md index 815b32b7..175be46f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,80 @@ Part of the LiquiFact stack: **frontend** (Next.js) | **backend** (this repo) | --- +## Environment Configuration + +All environment variables are **validated at startup**. The application will fail immediately with actionable error messages if required or invalid variables are detected. + +### Validation Behavior + +- **Required Variables**: None are strictly required; all have secure defaults. +- **Fast Failure**: Invalid configurations are caught at startup before the server starts. +- **Security**: Sensitive values (database URLs, credentials) are **never logged** in error messages. +- **Type Safety**: Environment variables are coerced to correct types (number, boolean, URL) and validated. + +### Environment Variables + +| Variable | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `PORT` | number | No | `3001` | HTTP server port (1-65535) | +| `NODE_ENV` | string | No | `development` | Runtime environment (development, production, test) | +| `STELLAR_NETWORK` | string | No | `testnet` | Stellar network (testnet, public) | +| `HORIZON_URL` | url | No | `https://horizon-testnet.stellar.org` | Horizon API base URL | +| `SOROBAN_RPC_URL` | url | No | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint URL | +| `DATABASE_URL` | url | No | null | PostgreSQL database connection URL (optional) | +| `REDIS_URL` | url | No | null | Redis cache connection URL (optional) | + +### Configuration Examples + +**Development (local)** +```bash +PORT=3001 +NODE_ENV=development +STELLAR_NETWORK=testnet +HORIZON_URL=http://localhost:11626 +SOROBAN_RPC_URL=http://localhost:8000 +``` + +**Production** +```bash +PORT=8080 +NODE_ENV=production +STELLAR_NETWORK=public +HORIZON_URL=https://horizon.stellar.org +SOROBAN_RPC_URL=https://soroban.stellar.org +DATABASE_URL=postgresql://user:pass@db.example.com:5432/liquifact +REDIS_URL=redis://:password@redis.example.com:6379 +``` + +### Validation Rules + +| Variable | Rules | +|----------|-------| +| `PORT` | Must be an integer between 1 and 65535 | +| `NODE_ENV` | Must be one of: `development`, `production`, `test` | +| `STELLAR_NETWORK` | Must be one of: `testnet`, `public` | +| `HORIZON_URL` | Must be a valid HTTP/HTTPS URL | +| `SOROBAN_RPC_URL` | Must be a valid HTTP/HTTPS URL | +| `DATABASE_URL` | Must be a valid database URL (postgresql, mysql, mongodb) | +| `REDIS_URL` | Must be a valid Redis URL | + +### Error Examples + +If configuration is invalid, the service will fail at startup with actionable messages: + +``` +Environment validation failed: + • Invalid type for PORT: Cannot convert to number: "abc" + • STELLAR_NETWORK must be one of: testnet, public + • HORIZON_URL must be a valid URL + +Please check your environment configuration. +``` + +No sensitive values (passwords, URLs) are shown in error messages for security. + +--- + ## Development | Command | Description | @@ -44,6 +118,7 @@ Part of the LiquiFact stack: **frontend** (Next.js) | **backend** (this repo) | | `npm run dev` | Start API with watch mode | | `npm run start`| Start API (production-style) | | `npm run lint` | Run ESLint on `src/` | +| `npm test` | Run Jest tests with coverage | Default port: **3001**. After starting: @@ -57,11 +132,14 @@ Default port: **3001**. After starting: ``` liquifact-backend/ ├── src/ +│ ├── config/ +│ │ ├── env.js # Environment validation schema and loader +│ │ └── env.test.js # Comprehensive env validation tests │ ├── services/ │ │ └── soroban.js # Contract interaction wrappers │ ├── utils/ │ │ └── retry.js # Exponential backoff utility -│ └── index.js # Express app, routes +│ └── index.js # Express app, routes, startup validation ├── .env.example # Env template ├── eslint.config.js └── package.json @@ -69,7 +147,69 @@ liquifact-backend/ --- -## Resiliency & Retries +## Testing + +This project maintains **95%+ test coverage** using Jest. + +### Running Tests + +```bash +# Run all tests with coverage report +npm test + +# Run tests in watch mode +npm test -- --watch + +# Run specific test file +npm test -- src/config/env.test.js +``` + +### Test Coverage + +The test suite includes: + +- **Configuration Validation** (`src/config/env.test.js`) + - Valid configurations (minimal, full, production, development) + - Type coercion and validation + - Port range validation (1-65535) + - Environment name validation + - Stellar network validation + - URL format and protocol validation + - Database URL validation (PostgreSQL, MySQL, MongoDB) + - Redis URL validation + - Error messages and security + - Edge cases (empty strings, whitespace, special characters) + - Missing and invalid variables + - Multiple validation errors + +- **Retry Utility** (`src/utils/retry.test.js`) + - Success on first try + - Retry on transient failures + - Failure after retries exhausted + - Security caps validation + - Jitter and backoff calculations + +### Security Notes + +**Environment Validation Security**: + +1. **Secure Defaults**: All variables either have safe defaults or are optional. +2. **No Credential Logging**: Database URLs, Redis URLs, and API keys are NEVER logged in error messages. +3. **Type Safety**: Values are coerced and validated to prevent injection attacks. +4. **Scope Isolation**: Environment variables are validated in isolation with no cross-script dependencies. +5. **Stderr Output**: Validation errors are sent to stderr, not stdout, preventing accidental logging to access logs. +6. **Fast Failure**: Invalid configurations are caught at startup, preventing degraded operation. +7. **Bounded Retries**: The retry utility has hard-capped retry counts and delays to prevent resource exhaustion. + +**Best Practices**: + +- Use strong, unique passwords in database/cache URLs. +- Rotate credentials regularly. +- Never commit `.env` files to version control. +- Use a secrets manager (AWS Secrets Manager, HashiCorp Vault) in production. +- Log validation errors to a secure audit trail, not to standard logs. + +--- To ensure reliable communication with Soroban contract provider APIs, this backend implements a robust **Retry and Backoff** mechanism (`src/utils/retry.js`). @@ -103,9 +243,11 @@ Ensure your branch passes these before opening a PR. 4. **Make changes**. Keep the style consistent: - Run `npm run lint` and fix any issues. - Use the existing Express/route patterns in `src/index.js`. -5. **Commit** with clear messages (e.g. `feat: add X`, `fix: Y`). -6. **Push** to your fork and open a **Pull Request** to `main`. -7. Wait for CI to pass and address any review feedback. + - Add or update tests as needed (see Testing section). +5. **Run tests**: `npm test` to ensure all tests pass and coverage remains at 95%+. +6. **Commit** with clear messages (e.g. `feat: add X`, `fix: Y`). +7. **Push** to your fork and open a **Pull Request** to `main`. +8. Wait for CI to pass and address any review feedback. We welcome docs improvements, bug fixes, and new API endpoints aligned with the LiquiFact product (invoices, escrow, Stellar integration). diff --git a/coverage/clover.xml b/coverage/clover.xml index a62bf81c..a0716fb6 100644 --- a/coverage/clover.xml +++ b/coverage/clover.xml @@ -1,10 +1,121 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -17,7 +128,7 @@ - + diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json index dd4a956a..c0344701 100644 --- a/coverage/coverage-final.json +++ b/coverage/coverage-final.json @@ -1,3 +1,4 @@ -{"/Users/victor/Desktop/Liquifact-backend/src/services/soroban.js": {"path":"/Users/victor/Desktop/Liquifact-backend/src/services/soroban.js","statementMap":{"0":{"start":{"line":1,"column":22},"end":{"line":1,"column":47}},"1":{"start":{"line":10,"column":28},"end":{"line":16,"column":3}},"2":{"start":{"line":18,"column":23},"end":{"line":18,"column":58}},"3":{"start":{"line":19,"column":2},"end":{"line":19,"column":67}},"4":{"start":{"line":19,"column":39},"end":{"line":19,"column":65}},"5":{"start":{"line":30,"column":25},"end":{"line":35,"column":3}},"6":{"start":{"line":37,"column":2},"end":{"line":37,"column":75}},"7":{"start":{"line":40,"column":0},"end":{"line":43,"column":2}}},"fnMap":{"0":{"name":"isTransientError","decl":{"start":{"line":9,"column":9},"end":{"line":9,"column":25}},"loc":{"start":{"line":9,"column":33},"end":{"line":20,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":19,"column":32},"end":{"line":19,"column":33}},"loc":{"start":{"line":19,"column":39},"end":{"line":19,"column":65}},"line":19},"2":{"name":"callSorobanContract","decl":{"start":{"line":29,"column":15},"end":{"line":29,"column":34}},"loc":{"start":{"line":29,"column":70},"end":{"line":38,"column":1}},"line":29}},"branchMap":{"0":{"loc":{"start":{"line":18,"column":24},"end":{"line":18,"column":43}},"type":"binary-expr","locations":[{"start":{"line":18,"column":24},"end":{"line":18,"column":37}},{"start":{"line":18,"column":41},"end":{"line":18,"column":43}}],"line":18},"1":{"loc":{"start":{"line":29,"column":51},"end":{"line":29,"column":68}},"type":"default-arg","locations":[{"start":{"line":29,"column":66},"end":{"line":29,"column":68}}],"line":29}},"s":{"0":1,"1":8,"2":8,"3":8,"4":53,"5":3,"6":3,"7":1},"f":{"0":8,"1":53,"2":3},"b":{"0":[8,0],"1":[2]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"567f46ecae7a61b163a581c5e3ec53a74eb93794"} -,"/Users/victor/Desktop/Liquifact-backend/src/utils/retry.js": {"path":"/Users/victor/Desktop/Liquifact-backend/src/utils/retry.js","statementMap":{"0":{"start":{"line":29,"column":26},"end":{"line":29,"column":28}},"1":{"start":{"line":30,"column":24},"end":{"line":30,"column":29}},"2":{"start":{"line":31,"column":29},"end":{"line":31,"column":34}},"3":{"start":{"line":38,"column":6},"end":{"line":38,"column":13}},"4":{"start":{"line":37,"column":24},"end":{"line":37,"column":28}},"5":{"start":{"line":41,"column":2},"end":{"line":41,"column":66}},"6":{"start":{"line":42,"column":2},"end":{"line":42,"column":67}},"7":{"start":{"line":43,"column":2},"end":{"line":43,"column":60}},"8":{"start":{"line":45,"column":16},"end":{"line":45,"column":17}},"9":{"start":{"line":47,"column":2},"end":{"line":65,"column":3}},"10":{"start":{"line":48,"column":4},"end":{"line":64,"column":5}},"11":{"start":{"line":49,"column":6},"end":{"line":49,"column":31}},"12":{"start":{"line":51,"column":6},"end":{"line":53,"column":7}},"13":{"start":{"line":52,"column":8},"end":{"line":52,"column":20}},"14":{"start":{"line":56,"column":31},"end":{"line":56,"column":63}},"15":{"start":{"line":57,"column":20},"end":{"line":57,"column":56}},"16":{"start":{"line":60,"column":28},"end":{"line":60,"column":63}},"17":{"start":{"line":62,"column":6},"end":{"line":62,"column":16}},"18":{"start":{"line":63,"column":6},"end":{"line":63,"column":73}},"19":{"start":{"line":63,"column":37},"end":{"line":63,"column":71}},"20":{"start":{"line":68,"column":0},"end":{"line":70,"column":2}}},"fnMap":{"0":{"name":"withRetry","decl":{"start":{"line":27,"column":15},"end":{"line":27,"column":24}},"loc":{"start":{"line":27,"column":50},"end":{"line":66,"column":1}},"line":27},"1":{"name":"(anonymous_1)","decl":{"start":{"line":37,"column":18},"end":{"line":37,"column":19}},"loc":{"start":{"line":37,"column":24},"end":{"line":37,"column":28}},"line":37},"2":{"name":"(anonymous_2)","decl":{"start":{"line":63,"column":24},"end":{"line":63,"column":25}},"loc":{"start":{"line":63,"column":37},"end":{"line":63,"column":71}},"line":63}},"branchMap":{"0":{"loc":{"start":{"line":27,"column":36},"end":{"line":27,"column":48}},"type":"default-arg","locations":[{"start":{"line":27,"column":46},"end":{"line":27,"column":48}}],"line":27},"1":{"loc":{"start":{"line":34,"column":4},"end":{"line":34,"column":18}},"type":"default-arg","locations":[{"start":{"line":34,"column":17},"end":{"line":34,"column":18}}],"line":34},"2":{"loc":{"start":{"line":35,"column":4},"end":{"line":35,"column":19}},"type":"default-arg","locations":[{"start":{"line":35,"column":16},"end":{"line":35,"column":19}}],"line":35},"3":{"loc":{"start":{"line":36,"column":4},"end":{"line":36,"column":20}},"type":"default-arg","locations":[{"start":{"line":36,"column":15},"end":{"line":36,"column":20}}],"line":36},"4":{"loc":{"start":{"line":37,"column":4},"end":{"line":37,"column":28}},"type":"default-arg","locations":[{"start":{"line":37,"column":18},"end":{"line":37,"column":28}}],"line":37},"5":{"loc":{"start":{"line":51,"column":6},"end":{"line":53,"column":7}},"type":"if","locations":[{"start":{"line":51,"column":6},"end":{"line":53,"column":7}},{"start":{},"end":{}}],"line":51},"6":{"loc":{"start":{"line":51,"column":10},"end":{"line":51,"column":54}},"type":"binary-expr","locations":[{"start":{"line":51,"column":10},"end":{"line":51,"column":31}},{"start":{"line":51,"column":35},"end":{"line":51,"column":54}}],"line":51}},"s":{"0":9,"1":9,"2":9,"3":9,"4":15,"5":9,"6":9,"7":9,"8":9,"9":9,"10":25,"11":25,"12":21,"13":5,"14":16,"15":16,"16":16,"17":16,"18":16,"19":16,"20":2},"f":{"0":9,"1":15,"2":16},"b":{"0":[1],"1":[1],"2":[1],"3":[5],"4":[5],"5":[5,16],"6":[21,18]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"117addf2b650bf8e8f2947fd0dc37f59480b0548"} +{"/Users/mac/Documents/Drip/Liquifact-backend/src/config/env.js": {"path":"/Users/mac/Documents/Drip/Liquifact-backend/src/config/env.js","statementMap":{"0":{"start":{"line":14,"column":19},"end":{"line":19,"column":1}},"1":{"start":{"line":25,"column":18},"end":{"line":30,"column":1}},"2":{"start":{"line":37,"column":19},"end":{"line":181,"column":1}},"3":{"start":{"line":43,"column":6},"end":{"line":45,"column":7}},"4":{"start":{"line":44,"column":8},"end":{"line":44,"column":110}},"5":{"start":{"line":46,"column":6},"end":{"line":46,"column":29}},"6":{"start":{"line":54,"column":24},"end":{"line":54,"column":61}},"7":{"start":{"line":55,"column":6},"end":{"line":61,"column":7}},"8":{"start":{"line":56,"column":8},"end":{"line":60,"column":10}},"9":{"start":{"line":62,"column":6},"end":{"line":62,"column":29}},"10":{"start":{"line":70,"column":28},"end":{"line":70,"column":49}},"11":{"start":{"line":71,"column":6},"end":{"line":77,"column":7}},"12":{"start":{"line":72,"column":8},"end":{"line":76,"column":10}},"13":{"start":{"line":78,"column":6},"end":{"line":78,"column":29}},"14":{"start":{"line":86,"column":6},"end":{"line":102,"column":7}},"15":{"start":{"line":87,"column":20},"end":{"line":87,"column":34}},"16":{"start":{"line":88,"column":8},"end":{"line":94,"column":9}},"17":{"start":{"line":89,"column":10},"end":{"line":93,"column":12}},"18":{"start":{"line":95,"column":8},"end":{"line":95,"column":31}},"19":{"start":{"line":97,"column":8},"end":{"line":101,"column":10}},"20":{"start":{"line":110,"column":6},"end":{"line":126,"column":7}},"21":{"start":{"line":111,"column":20},"end":{"line":111,"column":34}},"22":{"start":{"line":112,"column":8},"end":{"line":118,"column":9}},"23":{"start":{"line":113,"column":10},"end":{"line":117,"column":12}},"24":{"start":{"line":119,"column":8},"end":{"line":119,"column":31}},"25":{"start":{"line":121,"column":8},"end":{"line":125,"column":10}},"26":{"start":{"line":134,"column":6},"end":{"line":134,"column":41}},"27":{"start":{"line":134,"column":18},"end":{"line":134,"column":41}},"28":{"start":{"line":135,"column":6},"end":{"line":153,"column":7}},"29":{"start":{"line":136,"column":20},"end":{"line":136,"column":34}},"30":{"start":{"line":138,"column":31},"end":{"line":138,"column":97}},"31":{"start":{"line":139,"column":8},"end":{"line":145,"column":9}},"32":{"start":{"line":140,"column":10},"end":{"line":144,"column":12}},"33":{"start":{"line":146,"column":8},"end":{"line":146,"column":31}},"34":{"start":{"line":148,"column":8},"end":{"line":152,"column":10}},"35":{"start":{"line":161,"column":6},"end":{"line":161,"column":41}},"36":{"start":{"line":161,"column":18},"end":{"line":161,"column":41}},"37":{"start":{"line":162,"column":6},"end":{"line":178,"column":7}},"38":{"start":{"line":163,"column":20},"end":{"line":163,"column":34}},"39":{"start":{"line":164,"column":8},"end":{"line":170,"column":9}},"40":{"start":{"line":165,"column":10},"end":{"line":169,"column":12}},"41":{"start":{"line":171,"column":8},"end":{"line":171,"column":31}},"42":{"start":{"line":173,"column":8},"end":{"line":177,"column":10}},"43":{"start":{"line":192,"column":2},"end":{"line":194,"column":3}},"44":{"start":{"line":193,"column":4},"end":{"line":193,"column":16}},"45":{"start":{"line":196,"column":2},"end":{"line":226,"column":3}},"46":{"start":{"line":198,"column":18},"end":{"line":198,"column":31}},"47":{"start":{"line":199,"column":6},"end":{"line":201,"column":7}},"48":{"start":{"line":200,"column":8},"end":{"line":200,"column":64}},"49":{"start":{"line":202,"column":6},"end":{"line":202,"column":17}},"50":{"start":{"line":205,"column":6},"end":{"line":207,"column":7}},"51":{"start":{"line":206,"column":8},"end":{"line":206,"column":20}},"52":{"start":{"line":208,"column":6},"end":{"line":210,"column":7}},"53":{"start":{"line":209,"column":8},"end":{"line":209,"column":21}},"54":{"start":{"line":211,"column":6},"end":{"line":211,"column":63}},"55":{"start":{"line":215,"column":22},"end":{"line":215,"column":42}},"56":{"start":{"line":216,"column":6},"end":{"line":218,"column":7}},"57":{"start":{"line":217,"column":8},"end":{"line":217,"column":20}},"58":{"start":{"line":219,"column":6},"end":{"line":219,"column":21}},"59":{"start":{"line":222,"column":6},"end":{"line":222,"column":27}},"60":{"start":{"line":225,"column":6},"end":{"line":225,"column":19}},"61":{"start":{"line":239,"column":2},"end":{"line":245,"column":3}},"62":{"start":{"line":240,"column":4},"end":{"line":244,"column":6}},"63":{"start":{"line":248,"column":2},"end":{"line":253,"column":3}},"64":{"start":{"line":249,"column":4},"end":{"line":251,"column":5}},"65":{"start":{"line":250,"column":6},"end":{"line":250,"column":52}},"66":{"start":{"line":252,"column":4},"end":{"line":252,"column":40}},"67":{"start":{"line":257,"column":2},"end":{"line":265,"column":3}},"68":{"start":{"line":258,"column":4},"end":{"line":258,"column":49}},"69":{"start":{"line":260,"column":4},"end":{"line":264,"column":6}},"70":{"start":{"line":268,"column":2},"end":{"line":270,"column":3}},"71":{"start":{"line":269,"column":4},"end":{"line":269,"column":40}},"72":{"start":{"line":273,"column":2},"end":{"line":282,"column":3}},"73":{"start":{"line":274,"column":29},"end":{"line":274,"column":57}},"74":{"start":{"line":275,"column":4},"end":{"line":281,"column":5}},"75":{"start":{"line":276,"column":6},"end":{"line":280,"column":8}},"76":{"start":{"line":284,"column":2},"end":{"line":284,"column":45}},"77":{"start":{"line":301,"column":27},"end":{"line":301,"column":29}},"78":{"start":{"line":302,"column":17},"end":{"line":302,"column":19}},"79":{"start":{"line":304,"column":2},"end":{"line":317,"column":5}},"80":{"start":{"line":305,"column":18},"end":{"line":305,"column":26}},"81":{"start":{"line":306,"column":23},"end":{"line":306,"column":59}},"82":{"start":{"line":308,"column":4},"end":{"line":316,"column":5}},"83":{"start":{"line":309,"column":6},"end":{"line":313,"column":9}},"84":{"start":{"line":315,"column":6},"end":{"line":315,"column":37}},"85":{"start":{"line":320,"column":2},"end":{"line":331,"column":3}},"86":{"start":{"line":321,"column":26},"end":{"line":323,"column":17}},"87":{"start":{"line":322,"column":20},"end":{"line":322,"column":40}},"88":{"start":{"line":325,"column":18},"end":{"line":327,"column":5}},"89":{"start":{"line":328,"column":4},"end":{"line":328,"column":46}},"90":{"start":{"line":329,"column":4},"end":{"line":329,"column":37}},"91":{"start":{"line":330,"column":4},"end":{"line":330,"column":16}},"92":{"start":{"line":333,"column":2},"end":{"line":333,"column":16}},"93":{"start":{"line":342,"column":2},"end":{"line":342,"column":27}},"94":{"start":{"line":351,"column":12},"end":{"line":351,"column":43}},"95":{"start":{"line":352,"column":2},"end":{"line":352,"column":68}},"96":{"start":{"line":353,"column":2},"end":{"line":353,"column":68}},"97":{"start":{"line":355,"column":2},"end":{"line":362,"column":5}},"98":{"start":{"line":356,"column":17},"end":{"line":356,"column":28}},"99":{"start":{"line":357,"column":21},"end":{"line":357,"column":51}},"100":{"start":{"line":358,"column":25},"end":{"line":358,"column":82}},"101":{"start":{"line":359,"column":24},"end":{"line":359,"column":51}},"102":{"start":{"line":361,"column":4},"end":{"line":361,"column":89}},"103":{"start":{"line":364,"column":2},"end":{"line":364,"column":13}},"104":{"start":{"line":374,"column":23},"end":{"line":382,"column":3}},"105":{"start":{"line":384,"column":2},"end":{"line":384,"column":57}},"106":{"start":{"line":387,"column":0},"end":{"line":401,"column":2}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":42,"column":14},"end":{"line":42,"column":15}},"loc":{"start":{"line":42,"column":25},"end":{"line":47,"column":5}},"line":42},"1":{"name":"(anonymous_1)","decl":{"start":{"line":53,"column":14},"end":{"line":53,"column":15}},"loc":{"start":{"line":53,"column":25},"end":{"line":63,"column":5}},"line":53},"2":{"name":"(anonymous_2)","decl":{"start":{"line":69,"column":14},"end":{"line":69,"column":15}},"loc":{"start":{"line":69,"column":25},"end":{"line":79,"column":5}},"line":69},"3":{"name":"(anonymous_3)","decl":{"start":{"line":85,"column":14},"end":{"line":85,"column":15}},"loc":{"start":{"line":85,"column":25},"end":{"line":103,"column":5}},"line":85},"4":{"name":"(anonymous_4)","decl":{"start":{"line":109,"column":14},"end":{"line":109,"column":15}},"loc":{"start":{"line":109,"column":25},"end":{"line":127,"column":5}},"line":109},"5":{"name":"(anonymous_5)","decl":{"start":{"line":133,"column":14},"end":{"line":133,"column":15}},"loc":{"start":{"line":133,"column":25},"end":{"line":154,"column":5}},"line":133},"6":{"name":"(anonymous_6)","decl":{"start":{"line":160,"column":14},"end":{"line":160,"column":15}},"loc":{"start":{"line":160,"column":25},"end":{"line":179,"column":5}},"line":160},"7":{"name":"parseValue","decl":{"start":{"line":191,"column":9},"end":{"line":191,"column":19}},"loc":{"start":{"line":191,"column":33},"end":{"line":227,"column":1}},"line":191},"8":{"name":"validateVariable","decl":{"start":{"line":237,"column":9},"end":{"line":237,"column":25}},"loc":{"start":{"line":237,"column":46},"end":{"line":285,"column":1}},"line":237},"9":{"name":"validateEnv","decl":{"start":{"line":300,"column":9},"end":{"line":300,"column":20}},"loc":{"start":{"line":300,"column":40},"end":{"line":334,"column":1}},"line":300},"10":{"name":"(anonymous_10)","decl":{"start":{"line":304,"column":37},"end":{"line":304,"column":38}},"loc":{"start":{"line":304,"column":56},"end":{"line":317,"column":3}},"line":304},"11":{"name":"(anonymous_11)","decl":{"start":{"line":322,"column":11},"end":{"line":322,"column":12}},"loc":{"start":{"line":322,"column":20},"end":{"line":322,"column":40}},"line":322},"12":{"name":"getEnvSchema","decl":{"start":{"line":341,"column":9},"end":{"line":341,"column":21}},"loc":{"start":{"line":341,"column":24},"end":{"line":343,"column":1}},"line":341},"13":{"name":"generateEnvDocumentation","decl":{"start":{"line":350,"column":9},"end":{"line":350,"column":33}},"loc":{"start":{"line":350,"column":36},"end":{"line":365,"column":1}},"line":350},"14":{"name":"(anonymous_14)","decl":{"start":{"line":355,"column":37},"end":{"line":355,"column":38}},"loc":{"start":{"line":355,"column":56},"end":{"line":362,"column":3}},"line":355},"15":{"name":"getVariableDescription","decl":{"start":{"line":373,"column":9},"end":{"line":373,"column":31}},"loc":{"start":{"line":373,"column":37},"end":{"line":385,"column":1}},"line":373}},"branchMap":{"0":{"loc":{"start":{"line":43,"column":6},"end":{"line":45,"column":7}},"type":"if","locations":[{"start":{"line":43,"column":6},"end":{"line":45,"column":7}},{"start":{},"end":{}}],"line":43},"1":{"loc":{"start":{"line":43,"column":10},"end":{"line":43,"column":36}},"type":"binary-expr","locations":[{"start":{"line":43,"column":10},"end":{"line":43,"column":19}},{"start":{"line":43,"column":23},"end":{"line":43,"column":36}}],"line":43},"2":{"loc":{"start":{"line":55,"column":6},"end":{"line":61,"column":7}},"type":"if","locations":[{"start":{"line":55,"column":6},"end":{"line":61,"column":7}},{"start":{},"end":{}}],"line":55},"3":{"loc":{"start":{"line":71,"column":6},"end":{"line":77,"column":7}},"type":"if","locations":[{"start":{"line":71,"column":6},"end":{"line":77,"column":7}},{"start":{},"end":{}}],"line":71},"4":{"loc":{"start":{"line":88,"column":8},"end":{"line":94,"column":9}},"type":"if","locations":[{"start":{"line":88,"column":8},"end":{"line":94,"column":9}},{"start":{},"end":{}}],"line":88},"5":{"loc":{"start":{"line":112,"column":8},"end":{"line":118,"column":9}},"type":"if","locations":[{"start":{"line":112,"column":8},"end":{"line":118,"column":9}},{"start":{},"end":{}}],"line":112},"6":{"loc":{"start":{"line":134,"column":6},"end":{"line":134,"column":41}},"type":"if","locations":[{"start":{"line":134,"column":6},"end":{"line":134,"column":41}},{"start":{},"end":{}}],"line":134},"7":{"loc":{"start":{"line":139,"column":8},"end":{"line":145,"column":9}},"type":"if","locations":[{"start":{"line":139,"column":8},"end":{"line":145,"column":9}},{"start":{},"end":{}}],"line":139},"8":{"loc":{"start":{"line":161,"column":6},"end":{"line":161,"column":41}},"type":"if","locations":[{"start":{"line":161,"column":6},"end":{"line":161,"column":41}},{"start":{},"end":{}}],"line":161},"9":{"loc":{"start":{"line":164,"column":8},"end":{"line":170,"column":9}},"type":"if","locations":[{"start":{"line":164,"column":8},"end":{"line":170,"column":9}},{"start":{},"end":{}}],"line":164},"10":{"loc":{"start":{"line":192,"column":2},"end":{"line":194,"column":3}},"type":"if","locations":[{"start":{"line":192,"column":2},"end":{"line":194,"column":3}},{"start":{},"end":{}}],"line":192},"11":{"loc":{"start":{"line":192,"column":6},"end":{"line":192,"column":59}},"type":"binary-expr","locations":[{"start":{"line":192,"column":6},"end":{"line":192,"column":18}},{"start":{"line":192,"column":22},"end":{"line":192,"column":36}},{"start":{"line":192,"column":40},"end":{"line":192,"column":59}}],"line":192},"12":{"loc":{"start":{"line":196,"column":2},"end":{"line":226,"column":3}},"type":"switch","locations":[{"start":{"line":197,"column":4},"end":{"line":202,"column":17}},{"start":{"line":204,"column":4},"end":{"line":211,"column":63}},{"start":{"line":213,"column":4},"end":{"line":219,"column":21}},{"start":{"line":221,"column":4},"end":{"line":222,"column":27}},{"start":{"line":224,"column":4},"end":{"line":225,"column":19}}],"line":196},"13":{"loc":{"start":{"line":199,"column":6},"end":{"line":201,"column":7}},"type":"if","locations":[{"start":{"line":199,"column":6},"end":{"line":201,"column":7}},{"start":{},"end":{}}],"line":199},"14":{"loc":{"start":{"line":205,"column":6},"end":{"line":207,"column":7}},"type":"if","locations":[{"start":{"line":205,"column":6},"end":{"line":207,"column":7}},{"start":{},"end":{}}],"line":205},"15":{"loc":{"start":{"line":208,"column":6},"end":{"line":210,"column":7}},"type":"if","locations":[{"start":{"line":208,"column":6},"end":{"line":210,"column":7}},{"start":{},"end":{}}],"line":208},"16":{"loc":{"start":{"line":216,"column":6},"end":{"line":218,"column":7}},"type":"if","locations":[{"start":{"line":216,"column":6},"end":{"line":218,"column":7}},{"start":{},"end":{}}],"line":216},"17":{"loc":{"start":{"line":239,"column":2},"end":{"line":245,"column":3}},"type":"if","locations":[{"start":{"line":239,"column":2},"end":{"line":245,"column":3}},{"start":{},"end":{}}],"line":239},"18":{"loc":{"start":{"line":239,"column":6},"end":{"line":239,"column":80}},"type":"binary-expr","locations":[{"start":{"line":239,"column":6},"end":{"line":239,"column":21}},{"start":{"line":239,"column":26},"end":{"line":239,"column":45}},{"start":{"line":239,"column":49},"end":{"line":239,"column":61}},{"start":{"line":239,"column":65},"end":{"line":239,"column":79}}],"line":239},"19":{"loc":{"start":{"line":248,"column":2},"end":{"line":253,"column":3}},"type":"if","locations":[{"start":{"line":248,"column":2},"end":{"line":253,"column":3}},{"start":{},"end":{}}],"line":248},"20":{"loc":{"start":{"line":248,"column":6},"end":{"line":248,"column":59}},"type":"binary-expr","locations":[{"start":{"line":248,"column":6},"end":{"line":248,"column":25}},{"start":{"line":248,"column":29},"end":{"line":248,"column":41}},{"start":{"line":248,"column":45},"end":{"line":248,"column":59}}],"line":248},"21":{"loc":{"start":{"line":249,"column":4},"end":{"line":251,"column":5}},"type":"if","locations":[{"start":{"line":249,"column":4},"end":{"line":251,"column":5}},{"start":{},"end":{}}],"line":249},"22":{"loc":{"start":{"line":249,"column":8},"end":{"line":249,"column":63}},"type":"binary-expr","locations":[{"start":{"line":249,"column":8},"end":{"line":249,"column":36}},{"start":{"line":249,"column":40},"end":{"line":249,"column":63}}],"line":249},"23":{"loc":{"start":{"line":268,"column":2},"end":{"line":270,"column":3}},"type":"if","locations":[{"start":{"line":268,"column":2},"end":{"line":270,"column":3}},{"start":{},"end":{}}],"line":268},"24":{"loc":{"start":{"line":268,"column":6},"end":{"line":268,"column":46}},"type":"binary-expr","locations":[{"start":{"line":268,"column":6},"end":{"line":268,"column":26}},{"start":{"line":268,"column":30},"end":{"line":268,"column":46}}],"line":268},"25":{"loc":{"start":{"line":273,"column":2},"end":{"line":282,"column":3}},"type":"if","locations":[{"start":{"line":273,"column":2},"end":{"line":282,"column":3}},{"start":{},"end":{}}],"line":273},"26":{"loc":{"start":{"line":273,"column":6},"end":{"line":273,"column":45}},"type":"binary-expr","locations":[{"start":{"line":273,"column":6},"end":{"line":273,"column":21}},{"start":{"line":273,"column":25},"end":{"line":273,"column":45}}],"line":273},"27":{"loc":{"start":{"line":275,"column":4},"end":{"line":281,"column":5}},"type":"if","locations":[{"start":{"line":275,"column":4},"end":{"line":281,"column":5}},{"start":{},"end":{}}],"line":275},"28":{"loc":{"start":{"line":300,"column":21},"end":{"line":300,"column":38}},"type":"default-arg","locations":[{"start":{"line":300,"column":27},"end":{"line":300,"column":38}}],"line":300},"29":{"loc":{"start":{"line":308,"column":4},"end":{"line":316,"column":5}},"type":"if","locations":[{"start":{"line":308,"column":4},"end":{"line":316,"column":5}},{"start":{"line":314,"column":11},"end":{"line":316,"column":5}}],"line":308},"30":{"loc":{"start":{"line":320,"column":2},"end":{"line":331,"column":3}},"type":"if","locations":[{"start":{"line":320,"column":2},"end":{"line":331,"column":3}},{"start":{},"end":{}}],"line":320},"31":{"loc":{"start":{"line":357,"column":21},"end":{"line":357,"column":51}},"type":"cond-expr","locations":[{"start":{"line":357,"column":39},"end":{"line":357,"column":44}},{"start":{"line":357,"column":47},"end":{"line":357,"column":51}}],"line":357},"32":{"loc":{"start":{"line":358,"column":25},"end":{"line":358,"column":82}},"type":"cond-expr","locations":[{"start":{"line":358,"column":51},"end":{"line":358,"column":74}},{"start":{"line":358,"column":77},"end":{"line":358,"column":82}}],"line":358},"33":{"loc":{"start":{"line":384,"column":9},"end":{"line":384,"column":56}},"type":"binary-expr","locations":[{"start":{"line":384,"column":9},"end":{"line":384,"column":26}},{"start":{"line":384,"column":30},"end":{"line":384,"column":56}}],"line":384}},"s":{"0":1,"1":1,"2":1,"3":58,"4":5,"5":53,"6":59,"7":59,"8":3,"9":56,"10":59,"11":59,"12":2,"13":57,"14":61,"15":61,"16":60,"17":1,"18":59,"19":1,"20":61,"21":61,"22":60,"23":1,"24":59,"25":1,"26":11,"27":0,"28":11,"29":11,"30":9,"31":9,"32":1,"33":8,"34":2,"35":7,"36":0,"37":7,"38":7,"39":6,"40":1,"41":5,"42":1,"43":349,"44":3,"45":346,"46":63,"47":63,"48":5,"49":58,"50":18,"51":9,"52":9,"53":8,"54":1,"55":145,"56":145,"57":3,"58":142,"59":119,"60":1,"61":438,"62":1,"63":437,"64":115,"65":11,"66":104,"67":322,"68":322,"69":4,"70":318,"71":2,"72":316,"73":316,"74":316,"75":19,"76":297,"77":62,"78":62,"79":62,"80":434,"81":434,"82":434,"83":21,"84":413,"85":62,"86":19,"87":21,"88":19,"89":19,"90":19,"91":19,"92":43,"93":1,"94":2,"95":2,"96":2,"97":2,"98":14,"99":14,"100":14,"101":14,"102":14,"103":2,"104":14,"105":14,"106":1},"f":{"0":58,"1":59,"2":59,"3":61,"4":61,"5":11,"6":7,"7":349,"8":438,"9":62,"10":434,"11":21,"12":1,"13":2,"14":14,"15":14},"b":{"0":[5,53],"1":[58,56],"2":[3,56],"3":[2,57],"4":[1,59],"5":[1,59],"6":[0,11],"7":[1,8],"8":[0,7],"9":[1,5],"10":[3,346],"11":[349,348,347],"12":[63,18,145,119,1],"13":[5,58],"14":[9,9],"15":[8,1],"16":[3,142],"17":[1,437],"18":[438,1,1,0],"19":[115,322],"20":[437,329,322],"21":[11,104],"22":[115,115],"23":[2,316],"24":[318,2],"25":[316,0],"26":[316,316],"27":[19,297],"28":[0],"29":[21,413],"30":[19,43],"31":[0,14],"32":[10,4],"33":[14,0]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"cbc094a043eafac32d0c2ac37bd97e5d3a3ec64f"} +,"/Users/mac/Documents/Drip/Liquifact-backend/src/services/soroban.js": {"path":"/Users/mac/Documents/Drip/Liquifact-backend/src/services/soroban.js","statementMap":{"0":{"start":{"line":1,"column":22},"end":{"line":1,"column":47}},"1":{"start":{"line":10,"column":28},"end":{"line":16,"column":3}},"2":{"start":{"line":18,"column":23},"end":{"line":18,"column":58}},"3":{"start":{"line":19,"column":2},"end":{"line":19,"column":67}},"4":{"start":{"line":19,"column":39},"end":{"line":19,"column":65}},"5":{"start":{"line":30,"column":25},"end":{"line":35,"column":3}},"6":{"start":{"line":37,"column":2},"end":{"line":37,"column":75}},"7":{"start":{"line":40,"column":0},"end":{"line":43,"column":2}}},"fnMap":{"0":{"name":"isTransientError","decl":{"start":{"line":9,"column":9},"end":{"line":9,"column":25}},"loc":{"start":{"line":9,"column":33},"end":{"line":20,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":19,"column":32},"end":{"line":19,"column":33}},"loc":{"start":{"line":19,"column":39},"end":{"line":19,"column":65}},"line":19},"2":{"name":"callSorobanContract","decl":{"start":{"line":29,"column":15},"end":{"line":29,"column":34}},"loc":{"start":{"line":29,"column":70},"end":{"line":38,"column":1}},"line":29}},"branchMap":{"0":{"loc":{"start":{"line":18,"column":24},"end":{"line":18,"column":43}},"type":"binary-expr","locations":[{"start":{"line":18,"column":24},"end":{"line":18,"column":37}},{"start":{"line":18,"column":41},"end":{"line":18,"column":43}}],"line":18},"1":{"loc":{"start":{"line":29,"column":51},"end":{"line":29,"column":68}},"type":"default-arg","locations":[{"start":{"line":29,"column":66},"end":{"line":29,"column":68}}],"line":29}},"s":{"0":1,"1":8,"2":8,"3":8,"4":53,"5":3,"6":3,"7":1},"f":{"0":8,"1":53,"2":3},"b":{"0":[8,0],"1":[2]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"1daeb5e1d3999e37e3417799d5b00e587105d25b"} +,"/Users/mac/Documents/Drip/Liquifact-backend/src/utils/retry.js": {"path":"/Users/mac/Documents/Drip/Liquifact-backend/src/utils/retry.js","statementMap":{"0":{"start":{"line":29,"column":26},"end":{"line":29,"column":28}},"1":{"start":{"line":30,"column":24},"end":{"line":30,"column":29}},"2":{"start":{"line":31,"column":29},"end":{"line":31,"column":34}},"3":{"start":{"line":38,"column":6},"end":{"line":38,"column":13}},"4":{"start":{"line":37,"column":24},"end":{"line":37,"column":28}},"5":{"start":{"line":41,"column":2},"end":{"line":41,"column":66}},"6":{"start":{"line":42,"column":2},"end":{"line":42,"column":67}},"7":{"start":{"line":43,"column":2},"end":{"line":43,"column":60}},"8":{"start":{"line":45,"column":16},"end":{"line":45,"column":17}},"9":{"start":{"line":47,"column":2},"end":{"line":65,"column":3}},"10":{"start":{"line":48,"column":4},"end":{"line":64,"column":5}},"11":{"start":{"line":49,"column":6},"end":{"line":49,"column":31}},"12":{"start":{"line":51,"column":6},"end":{"line":53,"column":7}},"13":{"start":{"line":52,"column":8},"end":{"line":52,"column":20}},"14":{"start":{"line":56,"column":31},"end":{"line":56,"column":63}},"15":{"start":{"line":57,"column":20},"end":{"line":57,"column":56}},"16":{"start":{"line":60,"column":28},"end":{"line":60,"column":63}},"17":{"start":{"line":62,"column":6},"end":{"line":62,"column":16}},"18":{"start":{"line":63,"column":6},"end":{"line":63,"column":73}},"19":{"start":{"line":63,"column":37},"end":{"line":63,"column":71}},"20":{"start":{"line":68,"column":0},"end":{"line":70,"column":2}}},"fnMap":{"0":{"name":"withRetry","decl":{"start":{"line":27,"column":15},"end":{"line":27,"column":24}},"loc":{"start":{"line":27,"column":50},"end":{"line":66,"column":1}},"line":27},"1":{"name":"(anonymous_1)","decl":{"start":{"line":37,"column":18},"end":{"line":37,"column":19}},"loc":{"start":{"line":37,"column":24},"end":{"line":37,"column":28}},"line":37},"2":{"name":"(anonymous_2)","decl":{"start":{"line":63,"column":24},"end":{"line":63,"column":25}},"loc":{"start":{"line":63,"column":37},"end":{"line":63,"column":71}},"line":63}},"branchMap":{"0":{"loc":{"start":{"line":27,"column":36},"end":{"line":27,"column":48}},"type":"default-arg","locations":[{"start":{"line":27,"column":46},"end":{"line":27,"column":48}}],"line":27},"1":{"loc":{"start":{"line":34,"column":4},"end":{"line":34,"column":18}},"type":"default-arg","locations":[{"start":{"line":34,"column":17},"end":{"line":34,"column":18}}],"line":34},"2":{"loc":{"start":{"line":35,"column":4},"end":{"line":35,"column":19}},"type":"default-arg","locations":[{"start":{"line":35,"column":16},"end":{"line":35,"column":19}}],"line":35},"3":{"loc":{"start":{"line":36,"column":4},"end":{"line":36,"column":20}},"type":"default-arg","locations":[{"start":{"line":36,"column":15},"end":{"line":36,"column":20}}],"line":36},"4":{"loc":{"start":{"line":37,"column":4},"end":{"line":37,"column":28}},"type":"default-arg","locations":[{"start":{"line":37,"column":18},"end":{"line":37,"column":28}}],"line":37},"5":{"loc":{"start":{"line":51,"column":6},"end":{"line":53,"column":7}},"type":"if","locations":[{"start":{"line":51,"column":6},"end":{"line":53,"column":7}},{"start":{},"end":{}}],"line":51},"6":{"loc":{"start":{"line":51,"column":10},"end":{"line":51,"column":54}},"type":"binary-expr","locations":[{"start":{"line":51,"column":10},"end":{"line":51,"column":31}},{"start":{"line":51,"column":35},"end":{"line":51,"column":54}}],"line":51}},"s":{"0":9,"1":9,"2":9,"3":9,"4":15,"5":9,"6":9,"7":9,"8":9,"9":9,"10":25,"11":25,"12":21,"13":5,"14":16,"15":16,"16":16,"17":16,"18":16,"19":16,"20":2},"f":{"0":9,"1":15,"2":16},"b":{"0":[1],"1":[1],"2":[1],"3":[5],"4":[5],"5":[5,16],"6":[21,18]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"063908c3d88b545882357bfe8e03196dc70932e8"} } diff --git a/coverage/lcov-report/config/env.js.html b/coverage/lcov-report/config/env.js.html new file mode 100644 index 00000000..963539aa --- /dev/null +++ b/coverage/lcov-report/config/env.js.html @@ -0,0 +1,1288 @@ + + + + + + Code coverage report for config/env.js + + + + + + + + + +
+
+

All files / config env.js

+
+ +
+ 98.13% + Statements + 105/107 +
+ + +
+ 90.54% + Branches + 67/74 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 100% + Lines + 105/105 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +58x +5x +  +53x +  +  +  +  +  +  +  +59x +59x +3x +  +  +  +  +  +56x +  +  +  +  +  +  +  +59x +59x +2x +  +  +  +  +  +57x +  +  +  +  +  +  +  +61x +61x +60x +1x +  +  +  +  +  +59x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +61x +61x +60x +1x +  +  +  +  +  +59x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +11x +11x +11x +  +9x +9x +1x +  +  +  +  +  +8x +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +7x +7x +7x +6x +1x +  +  +  +  +  +5x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +349x +3x +  +  +346x +  +63x +63x +5x +  +58x +  +  +18x +9x +  +9x +8x +  +1x +  +  +  +145x +145x +3x +  +142x +  +  +119x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +438x +1x +  +  +  +  +  +  +  +437x +115x +11x +  +104x +  +  +  +  +322x +322x +  +4x +  +  +  +  +  +  +  +318x +2x +  +  +  +316x +316x +316x +19x +  +  +  +  +  +  +  +297x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +62x +62x +  +62x +434x +434x +  +434x +21x +  +  +  +  +  +413x +  +  +  +  +62x +19x +21x +  +  +19x +  +  +19x +19x +19x +  +  +43x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +2x +2x +2x +  +2x +14x +14x +14x +14x +  +14x +  +  +2x +  +  +  +  +  +  +  +  +  +14x +  +  +  +  +  +  +  +  +  +14x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Environment Configuration Validation Module
+ * 
+ * Validates required and optional environment variables at startup.
+ * Provides secure, actionable error messages without logging sensitive values.
+ * 
+ * @module config/env
+ */
+ 
+/**
+ * Enum for environment variable validation errors.
+ * @enum {string}
+ */
+const ENV_ERRORS = {
+  MISSING: 'MISSING',
+  INVALID_TYPE: 'INVALID_TYPE',
+  INVALID_VALUE: 'INVALID_VALUE',
+  INVALID_RANGE: 'INVALID_RANGE',
+};
+ 
+/**
+ * Enum for environment variable types.
+ * @enum {string}
+ */
+const ENV_TYPES = {
+  STRING: 'string',
+  NUMBER: 'number',
+  BOOLEAN: 'boolean',
+  URL: 'url',
+};
+ 
+/**
+ * Schema definition for all environment variables.
+ * Each variable specifies its type, whether it's required, default value, and validation rules.
+ * @type {Object<string, Object>}
+ */
+const ENV_SCHEMA = {
+  PORT: {
+    type: ENV_TYPES.NUMBER,
+    required: false,
+    default: 3001,
+    validate: (value) => {
+      if (value < 1 || value > 65535) {
+        return { valid: false, error: ENV_ERRORS.INVALID_RANGE, message: 'Port must be between 1 and 65535' };
+      }
+      return { valid: true };
+    },
+  },
+  NODE_ENV: {
+    type: ENV_TYPES.STRING,
+    required: false,
+    default: 'development',
+    validate: (value) => {
+      const validEnvs = ['development', 'production', 'test'];
+      if (!validEnvs.includes(value)) {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: `NODE_ENV must be one of: ${validEnvs.join(', ')}`,
+        };
+      }
+      return { valid: true };
+    },
+  },
+  STELLAR_NETWORK: {
+    type: ENV_TYPES.STRING,
+    required: false,
+    default: 'testnet',
+    validate: (value) => {
+      const validNetworks = ['testnet', 'public'];
+      if (!validNetworks.includes(value)) {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: `STELLAR_NETWORK must be one of: ${validNetworks.join(', ')}`,
+        };
+      }
+      return { valid: true };
+    },
+  },
+  HORIZON_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: 'https://horizon-testnet.stellar.org',
+    validate: (value) => {
+      try {
+        const url = new URL(value);
+        if (!['http:', 'https:'].includes(url.protocol)) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'HORIZON_URL must use http or https protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'HORIZON_URL must be a valid URL',
+        };
+      }
+    },
+  },
+  SOROBAN_RPC_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: 'https://soroban-testnet.stellar.org',
+    validate: (value) => {
+      try {
+        const url = new URL(value);
+        if (!['http:', 'https:'].includes(url.protocol)) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'SOROBAN_RPC_URL must use http or https protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'SOROBAN_RPC_URL must be a valid URL',
+        };
+      }
+    },
+  },
+  DATABASE_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: null,
+    validate: (value) => {
+      Iif (!value) return { valid: true };
+      try {
+        const url = new URL(value);
+        // Support postgresql, mysql, mongodb protocols
+        const validProtocols = ['postgresql:', 'postgres:', 'mysql:', 'mongodb:', 'mongodb+srv:'];
+        if (!validProtocols.includes(url.protocol)) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'DATABASE_URL must use a valid database protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'DATABASE_URL must be a valid database connection URL',
+        };
+      }
+    },
+  },
+  REDIS_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: null,
+    validate: (value) => {
+      Iif (!value) return { valid: true };
+      try {
+        const url = new URL(value);
+        if (!url.protocol.startsWith('redis')) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'REDIS_URL must use redis protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'REDIS_URL must be a valid Redis URL',
+        };
+      }
+    },
+  },
+};
+ 
+/**
+ * Parses a string environment value to the specified type.
+ * 
+ * @param {string} value - The string value from process.env
+ * @param {string} type - The target type from ENV_TYPES
+ * @returns {any} The parsed value
+ * @throws {Error} If type conversion fails
+ */
+function parseValue(value, type) {
+  if (value === '' || value === null || value === undefined) {
+    return null;
+  }
+ 
+  switch (type) {
+    case ENV_TYPES.NUMBER:
+      const num = Number(value);
+      if (Number.isNaN(num)) {
+        throw new Error(`Cannot convert to number: "${value}"`);
+      }
+      return num;
+ 
+    case ENV_TYPES.BOOLEAN:
+      if (['true', '1', 'yes', 'on'].includes(value.toLowerCase())) {
+        return true;
+      }
+      if (['false', '0', 'no', 'off'].includes(value.toLowerCase())) {
+        return false;
+      }
+      throw new Error(`Cannot convert to boolean: "${value}"`);
+ 
+    case ENV_TYPES.URL:
+      // Trim whitespace for URLs, treat whitespace-only as null
+      const trimmed = String(value).trim();
+      if (!trimmed) {
+        return null;
+      }
+      return trimmed;
+ 
+    case ENV_TYPES.STRING:
+      return String(value);
+ 
+    default:
+      return value;
+  }
+}
+ 
+/**
+ * Validates a single environment variable against its schema.
+ * 
+ * @param {string} key - The environment variable name
+ * @param {string} value - The environment variable value
+ * @param {Object} schema - The schema definition for this variable
+ * @returns {Object} { valid: boolean, value: any, error?: string, message?: string }
+ */
+function validateVariable(key, value, schema) {
+  // Check if required variable is missing
+  if (schema.required && (value === undefined || value === '' || value === null)) {
+    return {
+      valid: false,
+      error: ENV_ERRORS.MISSING,
+      message: `Missing required environment variable: ${key}`,
+    };
+  }
+ 
+  // Use default if not provided
+  if (value === undefined || value === '' || value === null) {
+    if (schema.default !== undefined && schema.default !== null) {
+      return { valid: true, value: schema.default };
+    }
+    return { valid: true, value: null };
+  }
+ 
+  // Parse the value to the correct type
+  let parsedValue;
+  try {
+    parsedValue = parseValue(value, schema.type);
+  } catch (e) {
+    return {
+      valid: false,
+      error: ENV_ERRORS.INVALID_TYPE,
+      message: `Invalid type for ${key}: ${e.message}`,
+    };
+  }
+ 
+  // Skip validation for null values when not required
+  if (parsedValue === null && !schema.required) {
+    return { valid: true, value: null };
+  }
+ 
+  // Run custom validation if provided
+  Eif (schema.validate && parsedValue !== null) {
+    const validationResult = schema.validate(parsedValue);
+    if (!validationResult.valid) {
+      return {
+        valid: false,
+        error: validationResult.error,
+        message: validationResult.message,
+      };
+    }
+  }
+ 
+  return { valid: true, value: parsedValue };
+}
+ 
+/**
+ * Validates all environment variables against the schema.
+ * Throws an error with actionable message if validation fails.
+ * 
+ * Security Note:
+ * - Sensitive values (DATABASE_URL, REDIS_URL, API keys) are NEVER logged
+ * - Error messages contain only variable names and type info, not values
+ * - Use stderr for failures to avoid logging to stdout
+ * 
+ * @param {Object} [env=process.env] - The environment object to validate (defaults to process.env)
+ * @returns {Object} The validated and parsed environment configuration
+ * @throws {Error} If validation fails, with detailed actionable message
+ */
+function validateEnv(env = process.env) {
+  const validationErrors = [];
+  const config = {};
+ 
+  Object.entries(ENV_SCHEMA).forEach(([key, schema]) => {
+    const value = env[key];
+    const validation = validateVariable(key, value, schema);
+ 
+    if (!validation.valid) {
+      validationErrors.push({
+        key,
+        error: validation.error,
+        message: validation.message,
+      });
+    } else {
+      config[key] = validation.value;
+    }
+  });
+ 
+  // If there are errors, throw with detailed message
+  if (validationErrors.length > 0) {
+    const errorMessages = validationErrors
+      .map((err) => `  • ${err.message}`)
+      .join('\n');
+ 
+    const error = new Error(
+      `Environment validation failed:\n${errorMessages}\n\nPlease check your environment configuration.`
+    );
+    error.name = 'EnvironmentValidationError';
+    error.details = validationErrors;
+    throw error;
+  }
+ 
+  return config;
+}
+ 
+/**
+ * Gets the environment validation schema for documentation purposes.
+ * 
+ * @returns {Object} A copy of the ENV_SCHEMA
+ */
+function getEnvSchema() {
+  return { ...ENV_SCHEMA };
+}
+ 
+/**
+ * Generates markdown documentation for environment variables.
+ * 
+ * @returns {string} Markdown formatted environment variable documentation
+ */
+function generateEnvDocumentation() {
+  let doc = '### Environment Variables\n\n';
+  doc += '| Variable | Type | Required | Default | Description |\n';
+  doc += '|----------|------|----------|---------|-------------|\n';
+ 
+  Object.entries(ENV_SCHEMA).forEach(([key, schema]) => {
+    const type = schema.type;
+    const required = schema.required ? 'Yes' : 'No';
+    const defaultValue = schema.default !== null ? `\`${schema.default}\`` : 'N/A';
+    const description = getVariableDescription(key);
+ 
+    doc += `| \`${key}\` | ${type} | ${required} | ${defaultValue} | ${description} |\n`;
+  });
+ 
+  return doc;
+}
+ 
+/**
+ * Returns a human-readable description for each environment variable.
+ * 
+ * @param {string} key - The environment variable name
+ * @returns {string} Description of the variable
+ */
+function getVariableDescription(key) {
+  const descriptions = {
+    PORT: 'HTTP server port (1-65535)',
+    NODE_ENV: 'Runtime environment (development, production, test)',
+    STELLAR_NETWORK: 'Stellar network (testnet, public)',
+    HORIZON_URL: 'Horizon API base URL',
+    SOROBAN_RPC_URL: 'Soroban RPC endpoint URL',
+    DATABASE_URL: 'PostgreSQL database connection URL (optional)',
+    REDIS_URL: 'Redis cache connection URL (optional)',
+  };
+ 
+  return descriptions[key] || 'No description available';
+}
+ 
+module.exports = {
+  // Main validation function
+  validateEnv,
+ 
+  // Export schema and utilities for testing
+  getEnvSchema,
+  generateEnvDocumentation,
+  parseValue,
+  validateVariable,
+ 
+  // Export enums for testing and documentation
+  ENV_ERRORS,
+  ENV_TYPES,
+  ENV_SCHEMA,
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/config/index.html b/coverage/lcov-report/config/index.html new file mode 100644 index 00000000..d3455581 --- /dev/null +++ b/coverage/lcov-report/config/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for config + + + + + + + + + +
+
+

All files config

+
+ +
+ 98.13% + Statements + 105/107 +
+ + +
+ 90.54% + Branches + 67/74 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 100% + Lines + 105/105 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
env.js +
+
98.13%105/10790.54%67/74100%16/16100%105/105
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/env.js.html b/coverage/lcov-report/env.js.html new file mode 100644 index 00000000..0d5a10d3 --- /dev/null +++ b/coverage/lcov-report/env.js.html @@ -0,0 +1,1288 @@ + + + + + + Code coverage report for env.js + + + + + + + + + +
+
+

All files env.js

+
+ +
+ 98.13% + Statements + 105/107 +
+ + +
+ 90.54% + Branches + 67/74 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 100% + Lines + 105/105 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +58x +5x +  +53x +  +  +  +  +  +  +  +59x +59x +3x +  +  +  +  +  +56x +  +  +  +  +  +  +  +59x +59x +2x +  +  +  +  +  +57x +  +  +  +  +  +  +  +61x +61x +60x +1x +  +  +  +  +  +59x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +61x +61x +60x +1x +  +  +  +  +  +59x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +11x +11x +11x +  +9x +9x +1x +  +  +  +  +  +8x +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +7x +7x +7x +6x +1x +  +  +  +  +  +5x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +349x +3x +  +  +346x +  +63x +63x +5x +  +58x +  +  +18x +9x +  +9x +8x +  +1x +  +  +  +145x +145x +3x +  +142x +  +  +119x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +438x +1x +  +  +  +  +  +  +  +437x +115x +11x +  +104x +  +  +  +  +322x +322x +  +4x +  +  +  +  +  +  +  +318x +2x +  +  +  +316x +316x +316x +19x +  +  +  +  +  +  +  +297x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +62x +62x +  +62x +434x +434x +  +434x +21x +  +  +  +  +  +413x +  +  +  +  +62x +19x +21x +  +  +19x +  +  +19x +19x +19x +  +  +43x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +2x +2x +2x +  +2x +14x +14x +14x +14x +  +14x +  +  +2x +  +  +  +  +  +  +  +  +  +14x +  +  +  +  +  +  +  +  +  +14x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Environment Configuration Validation Module
+ * 
+ * Validates required and optional environment variables at startup.
+ * Provides secure, actionable error messages without logging sensitive values.
+ * 
+ * @module config/env
+ */
+ 
+/**
+ * Enum for environment variable validation errors.
+ * @enum {string}
+ */
+const ENV_ERRORS = {
+  MISSING: 'MISSING',
+  INVALID_TYPE: 'INVALID_TYPE',
+  INVALID_VALUE: 'INVALID_VALUE',
+  INVALID_RANGE: 'INVALID_RANGE',
+};
+ 
+/**
+ * Enum for environment variable types.
+ * @enum {string}
+ */
+const ENV_TYPES = {
+  STRING: 'string',
+  NUMBER: 'number',
+  BOOLEAN: 'boolean',
+  URL: 'url',
+};
+ 
+/**
+ * Schema definition for all environment variables.
+ * Each variable specifies its type, whether it's required, default value, and validation rules.
+ * @type {Object<string, Object>}
+ */
+const ENV_SCHEMA = {
+  PORT: {
+    type: ENV_TYPES.NUMBER,
+    required: false,
+    default: 3001,
+    validate: (value) => {
+      if (value < 1 || value > 65535) {
+        return { valid: false, error: ENV_ERRORS.INVALID_RANGE, message: 'Port must be between 1 and 65535' };
+      }
+      return { valid: true };
+    },
+  },
+  NODE_ENV: {
+    type: ENV_TYPES.STRING,
+    required: false,
+    default: 'development',
+    validate: (value) => {
+      const validEnvs = ['development', 'production', 'test'];
+      if (!validEnvs.includes(value)) {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: `NODE_ENV must be one of: ${validEnvs.join(', ')}`,
+        };
+      }
+      return { valid: true };
+    },
+  },
+  STELLAR_NETWORK: {
+    type: ENV_TYPES.STRING,
+    required: false,
+    default: 'testnet',
+    validate: (value) => {
+      const validNetworks = ['testnet', 'public'];
+      if (!validNetworks.includes(value)) {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: `STELLAR_NETWORK must be one of: ${validNetworks.join(', ')}`,
+        };
+      }
+      return { valid: true };
+    },
+  },
+  HORIZON_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: 'https://horizon-testnet.stellar.org',
+    validate: (value) => {
+      try {
+        const url = new URL(value);
+        if (!['http:', 'https:'].includes(url.protocol)) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'HORIZON_URL must use http or https protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'HORIZON_URL must be a valid URL',
+        };
+      }
+    },
+  },
+  SOROBAN_RPC_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: 'https://soroban-testnet.stellar.org',
+    validate: (value) => {
+      try {
+        const url = new URL(value);
+        if (!['http:', 'https:'].includes(url.protocol)) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'SOROBAN_RPC_URL must use http or https protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'SOROBAN_RPC_URL must be a valid URL',
+        };
+      }
+    },
+  },
+  DATABASE_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: null,
+    validate: (value) => {
+      Iif (!value) return { valid: true };
+      try {
+        const url = new URL(value);
+        // Support postgresql, mysql, mongodb protocols
+        const validProtocols = ['postgresql:', 'postgres:', 'mysql:', 'mongodb:', 'mongodb+srv:'];
+        if (!validProtocols.includes(url.protocol)) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'DATABASE_URL must use a valid database protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'DATABASE_URL must be a valid database connection URL',
+        };
+      }
+    },
+  },
+  REDIS_URL: {
+    type: ENV_TYPES.URL,
+    required: false,
+    default: null,
+    validate: (value) => {
+      Iif (!value) return { valid: true };
+      try {
+        const url = new URL(value);
+        if (!url.protocol.startsWith('redis')) {
+          return {
+            valid: false,
+            error: ENV_ERRORS.INVALID_VALUE,
+            message: 'REDIS_URL must use redis protocol',
+          };
+        }
+        return { valid: true };
+      } catch {
+        return {
+          valid: false,
+          error: ENV_ERRORS.INVALID_VALUE,
+          message: 'REDIS_URL must be a valid Redis URL',
+        };
+      }
+    },
+  },
+};
+ 
+/**
+ * Parses a string environment value to the specified type.
+ * 
+ * @param {string} value - The string value from process.env
+ * @param {string} type - The target type from ENV_TYPES
+ * @returns {any} The parsed value
+ * @throws {Error} If type conversion fails
+ */
+function parseValue(value, type) {
+  if (value === '' || value === null || value === undefined) {
+    return null;
+  }
+ 
+  switch (type) {
+    case ENV_TYPES.NUMBER:
+      const num = Number(value);
+      if (Number.isNaN(num)) {
+        throw new Error(`Cannot convert to number: "${value}"`);
+      }
+      return num;
+ 
+    case ENV_TYPES.BOOLEAN:
+      if (['true', '1', 'yes', 'on'].includes(value.toLowerCase())) {
+        return true;
+      }
+      if (['false', '0', 'no', 'off'].includes(value.toLowerCase())) {
+        return false;
+      }
+      throw new Error(`Cannot convert to boolean: "${value}"`);
+ 
+    case ENV_TYPES.URL:
+      // Trim whitespace for URLs, treat whitespace-only as null
+      const trimmed = String(value).trim();
+      if (!trimmed) {
+        return null;
+      }
+      return trimmed;
+ 
+    case ENV_TYPES.STRING:
+      return String(value);
+ 
+    default:
+      return value;
+  }
+}
+ 
+/**
+ * Validates a single environment variable against its schema.
+ * 
+ * @param {string} key - The environment variable name
+ * @param {string} value - The environment variable value
+ * @param {Object} schema - The schema definition for this variable
+ * @returns {Object} { valid: boolean, value: any, error?: string, message?: string }
+ */
+function validateVariable(key, value, schema) {
+  // Check if required variable is missing
+  if (schema.required && (value === undefined || value === '' || value === null)) {
+    return {
+      valid: false,
+      error: ENV_ERRORS.MISSING,
+      message: `Missing required environment variable: ${key}`,
+    };
+  }
+ 
+  // Use default if not provided
+  if (value === undefined || value === '' || value === null) {
+    if (schema.default !== undefined && schema.default !== null) {
+      return { valid: true, value: schema.default };
+    }
+    return { valid: true, value: null };
+  }
+ 
+  // Parse the value to the correct type
+  let parsedValue;
+  try {
+    parsedValue = parseValue(value, schema.type);
+  } catch (e) {
+    return {
+      valid: false,
+      error: ENV_ERRORS.INVALID_TYPE,
+      message: `Invalid type for ${key}: ${e.message}`,
+    };
+  }
+ 
+  // Skip validation for null values when not required
+  if (parsedValue === null && !schema.required) {
+    return { valid: true, value: null };
+  }
+ 
+  // Run custom validation if provided
+  Eif (schema.validate && parsedValue !== null) {
+    const validationResult = schema.validate(parsedValue);
+    if (!validationResult.valid) {
+      return {
+        valid: false,
+        error: validationResult.error,
+        message: validationResult.message,
+      };
+    }
+  }
+ 
+  return { valid: true, value: parsedValue };
+}
+ 
+/**
+ * Validates all environment variables against the schema.
+ * Throws an error with actionable message if validation fails.
+ * 
+ * Security Note:
+ * - Sensitive values (DATABASE_URL, REDIS_URL, API keys) are NEVER logged
+ * - Error messages contain only variable names and type info, not values
+ * - Use stderr for failures to avoid logging to stdout
+ * 
+ * @param {Object} [env=process.env] - The environment object to validate (defaults to process.env)
+ * @returns {Object} The validated and parsed environment configuration
+ * @throws {Error} If validation fails, with detailed actionable message
+ */
+function validateEnv(env = process.env) {
+  const validationErrors = [];
+  const config = {};
+ 
+  Object.entries(ENV_SCHEMA).forEach(([key, schema]) => {
+    const value = env[key];
+    const validation = validateVariable(key, value, schema);
+ 
+    if (!validation.valid) {
+      validationErrors.push({
+        key,
+        error: validation.error,
+        message: validation.message,
+      });
+    } else {
+      config[key] = validation.value;
+    }
+  });
+ 
+  // If there are errors, throw with detailed message
+  if (validationErrors.length > 0) {
+    const errorMessages = validationErrors
+      .map((err) => `  • ${err.message}`)
+      .join('\n');
+ 
+    const error = new Error(
+      `Environment validation failed:\n${errorMessages}\n\nPlease check your environment configuration.`
+    );
+    error.name = 'EnvironmentValidationError';
+    error.details = validationErrors;
+    throw error;
+  }
+ 
+  return config;
+}
+ 
+/**
+ * Gets the environment validation schema for documentation purposes.
+ * 
+ * @returns {Object} A copy of the ENV_SCHEMA
+ */
+function getEnvSchema() {
+  return { ...ENV_SCHEMA };
+}
+ 
+/**
+ * Generates markdown documentation for environment variables.
+ * 
+ * @returns {string} Markdown formatted environment variable documentation
+ */
+function generateEnvDocumentation() {
+  let doc = '### Environment Variables\n\n';
+  doc += '| Variable | Type | Required | Default | Description |\n';
+  doc += '|----------|------|----------|---------|-------------|\n';
+ 
+  Object.entries(ENV_SCHEMA).forEach(([key, schema]) => {
+    const type = schema.type;
+    const required = schema.required ? 'Yes' : 'No';
+    const defaultValue = schema.default !== null ? `\`${schema.default}\`` : 'N/A';
+    const description = getVariableDescription(key);
+ 
+    doc += `| \`${key}\` | ${type} | ${required} | ${defaultValue} | ${description} |\n`;
+  });
+ 
+  return doc;
+}
+ 
+/**
+ * Returns a human-readable description for each environment variable.
+ * 
+ * @param {string} key - The environment variable name
+ * @returns {string} Description of the variable
+ */
+function getVariableDescription(key) {
+  const descriptions = {
+    PORT: 'HTTP server port (1-65535)',
+    NODE_ENV: 'Runtime environment (development, production, test)',
+    STELLAR_NETWORK: 'Stellar network (testnet, public)',
+    HORIZON_URL: 'Horizon API base URL',
+    SOROBAN_RPC_URL: 'Soroban RPC endpoint URL',
+    DATABASE_URL: 'PostgreSQL database connection URL (optional)',
+    REDIS_URL: 'Redis cache connection URL (optional)',
+  };
+ 
+  return descriptions[key] || 'No description available';
+}
+ 
+module.exports = {
+  // Main validation function
+  validateEnv,
+ 
+  // Export schema and utilities for testing
+  getEnvSchema,
+  generateEnvDocumentation,
+  parseValue,
+  validateVariable,
+ 
+  // Export enums for testing and documentation
+  ENV_ERRORS,
+  ENV_TYPES,
+  ENV_SCHEMA,
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html index c8c295fc..bccc8117 100644 --- a/coverage/lcov-report/index.html +++ b/coverage/lcov-report/index.html @@ -23,30 +23,30 @@

All files

- 100% + 98.52% Statements - 29/29 + 134/136
- 91.66% + 90.69% Branches - 11/12 + 78/86
100% Functions - 6/6 + 22/22
100% Lines - 27/27 + 132/132
@@ -79,6 +79,21 @@

All files

+ config + +
+ + 98.13% + 105/107 + 90.54% + 67/74 + 100% + 16/16 + 100% + 105/105 + + + services
@@ -116,7 +131,7 @@

All files