├── .gitignore ├── examples ├── context-path.js ├── basic.js └── auth.js ├── .github └── workflows │ └── node.js.yml ├── lib ├── spring.js └── config.js ├── package.json ├── LICENSE ├── tests ├── cert.pem ├── key.pem ├── fixtures.json └── client.spec.js ├── index.d.ts ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules/ 3 | .idea/ 4 | doc/ 5 | .vscode/ 6 | coverage/ 7 | .nyc_output/ 8 | -------------------------------------------------------------------------------- /examples/context-path.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const client = require('..') 4 | const options = { 5 | endpoint: 'http://localhost:8888/justapath', 6 | application: 'demo', 7 | profiles: ['test', 'timeout'] 8 | } 9 | 10 | // Using promise 11 | client.load(options).then((cfg) => { 12 | console.log(cfg.get('test.users', 'multi.uid')) 13 | console.log(cfg.toString(2)) 14 | }).catch((error) => console.error(error)) 15 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const client = require('..') 4 | const options = { 5 | application: 'demo', 6 | profiles: ['test', 'timeout'] 7 | } 8 | 9 | // Using promise 10 | client.load(options).then((cfg) => { 11 | console.log(cfg.get('test.users', 'multi.uid')) 12 | console.log(cfg.toString(2)) 13 | }).catch((error) => console.error(error)) 14 | 15 | // Using callback 16 | client.load(options, (error, cfg) => { 17 | if (error) { 18 | return console.error(error) 19 | } 20 | console.log(cfg.get('test.users', 'multi.uid')) 21 | console.log(cfg.toString(2)) 22 | }) 23 | -------------------------------------------------------------------------------- /examples/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const client = require('..') 4 | 5 | // Explicit basic auth 6 | const options1 = { 7 | application: 'demo', 8 | profiles: ['test', 'timeout'], 9 | auth: { 10 | user: 'username', 11 | pass: 'password' 12 | } 13 | } 14 | 15 | client.load(options1).then((cfg) => { 16 | console.log(cfg.get('test.users', 'multi.uid')) 17 | console.log(cfg.toString(2)) 18 | }).catch((error) => console.error(error)) 19 | 20 | // Implicit basic auth 21 | const options2 = { 22 | endpoint: 'http://user:pass@localhost:8888', 23 | application: 'demo', 24 | profiles: ['test', 'timeout'] 25 | } 26 | 27 | client.load(options2).then((cfg) => { 28 | console.log(cfg.get('test.users', 'multi.uid')) 29 | console.log(cfg.toString(2)) 30 | }).catch((error) => console.error(error)) 31 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /lib/spring.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @module Spring 5 | */ 6 | 7 | /** 8 | * Alias for configuration properties map 9 | * 10 | * @typedef {Map} ConfigSource 11 | */ 12 | 13 | /** 14 | * Spring configuration file 15 | * 16 | * @typedef {Object} ConfigFile 17 | * @property {string} name - file name 18 | * @property {module:Spring~ConfigSource} source - configuration properties 19 | */ 20 | 21 | /** 22 | * Spring configuration response data 23 | * 24 | * @typedef {Object} ConfigData 25 | * @property {string} name - application name 26 | * @property {Array} profiles - list of profiles included 27 | * @property {string} label - environment label 28 | * @property {string} version - commit hash of properties 29 | * @property {Array} propertySources - properties included for application, 30 | * sorted by more priority 31 | */ 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud-config-client", 3 | "version": "1.6.2", 4 | "description": "Spring Cloud Config Client for NodeJS", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "lint": "standard", 9 | "test:unit": "mocha tests/*.spec.js", 10 | "test": "npm run lint && nyc npm run test:unit", 11 | "doc": "jsdoc index.js lib --pedantic --verbose -R README.md -d doc" 12 | }, 13 | "nyc": { 14 | "reporter": [ 15 | "lcov", 16 | "text" 17 | ] 18 | }, 19 | "keywords": [ 20 | "Spring Cloud Config", 21 | "configuration", 22 | "client" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/victorherraiz/cloud-config-client.git" 27 | }, 28 | "author": "Víctor Herraiz Posada (https://github.com/victorherraiz)", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@types/node": "^17.0.23", 32 | "jsdoc": "^3.6.7", 33 | "mocha": "^9.1.1", 34 | "nyc": "^15.1.0", 35 | "standard": "^16.0.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Víctor Herraiz Posada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDtTCCAp2gAwIBAgIJAL8BdNp5VFcwMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTcwNDI3MjEyODU2WhcNMTgwNDI3MjEyODU2WjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEAnRMNcsqlSYhZKdBQfRzXGSbCPZ5WYKZjRG7Wx+y699Rl747FBHG5VwMI 8 | cTqhmFiG7hAUG/9iEq65qTxmBAGF2rYD8CeFc9a4wbkIrOaaufOCPK1TvMeLMD5l 9 | D33uqEJI1+VR7AjeE78oAqkjkeV0tPUcM012JsNq8nUKjWn4IqIQBQFyKdSFwsQ1 10 | B/2Mm/RtHBI0J+Gqc8ziHFdILcUThmPw05G2gl6uv9dkihlmdjsKmBDUBUpgLb/Z 11 | S1PONxP9lyz7F65HAOrjNJ4qiVzMySp5RVCpmiikO+9fu8QHmLoqOH55NZnGPKD/ 12 | As/Pz09TdUBEQzU/sd7GGp0nIkyWCQIDAQABo4GnMIGkMB0GA1UdDgQWBBS9Amel 13 | obge340v2Kh8RollZlE02TB1BgNVHSMEbjBsgBS9Amelobge340v2Kh8RollZlE0 14 | 2aFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV 15 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAL8BdNp5VFcwMAwGA1UdEwQF 16 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJlPQ1A0R1GH+Fsk3UxNA6H8zxW5P3Eq 17 | aTipeh5bWSgOgWdaM2xgBdJh67SMG9uk8C3uHghaQ7J7FXbOvKZwK+PR+XN9d/hQ 18 | 6pHcWZgeXV5JMfxrWzFMoOE53XpEy+MRZJclagz2eJzAlxfb+EkwhjpMbRIaWWoH 19 | ktDYkO2MsQR6R0zN0vtYC5//4X9oXY8o4EtxdqZmcsXCBPa6zcekSzo3rrXPAIQJ 20 | ZoA9wK9dlLFH0pmwwlirgBOV+G4p484JD8ZS99M3kEjIe2TiIUifhqgSDFmh6X0M 21 | Vk+UbOyFzVQVnH1VHCUpVU98bDMxmV0nHqbwKv+3hNTZzxscd5ak0Kc= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /tests/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAnRMNcsqlSYhZKdBQfRzXGSbCPZ5WYKZjRG7Wx+y699Rl747F 3 | BHG5VwMIcTqhmFiG7hAUG/9iEq65qTxmBAGF2rYD8CeFc9a4wbkIrOaaufOCPK1T 4 | vMeLMD5lD33uqEJI1+VR7AjeE78oAqkjkeV0tPUcM012JsNq8nUKjWn4IqIQBQFy 5 | KdSFwsQ1B/2Mm/RtHBI0J+Gqc8ziHFdILcUThmPw05G2gl6uv9dkihlmdjsKmBDU 6 | BUpgLb/ZS1PONxP9lyz7F65HAOrjNJ4qiVzMySp5RVCpmiikO+9fu8QHmLoqOH55 7 | NZnGPKD/As/Pz09TdUBEQzU/sd7GGp0nIkyWCQIDAQABAoIBACbTttdRUFpE4gV8 8 | AOlsX59P/WPN5/wsJQ2deGojEnSAhFIbMIhQtEfV8BhNLfTCrVfbkZz6G/wSRMKY 9 | s376AWR26bJLkql3wmPgoUxcFJMyplbpYXGgwb9DLSgPMRdWizsS7JUz+/FVp0ZB 10 | GRHPbnHsxPPJihM66wzT8a/TKgslMTor/qzNXWsWOVD8vCwK7nrc3U4uQPkDRGt9 11 | v+D1xVgc72dZLKE+y0o8BfB5IMLhveGnB5hZqASMZYnUYPxBVh0jpuqSPoDa5HDt 12 | J8abF/zStaWY8Aq78pGPXK4GE68qszplDKldD1mYe35oQ/iKB3Ie9W07jsyj+HrW 13 | 6pFQuaECgYEAy7pa1ioioBbhKbKQkH6TwPsWtJS7TcLxYuTls194pN/rTZQLVylH 14 | Du3kUdi5Yqll3eljARwMpSTdWmuTx8UDvBLAlctVWSybLPg3lQqiw4RfNZv1K0UI 15 | OCmCInXdD+iCa6zAOKDoeH2rVA4R+SYVMFktLEO3g+RqXhsiJoKdlB8CgYEAxWBP 16 | /5rf4PycjxNWx6NlkBk3zENy/F7e0w1wIyzFcVaqE1CVGcdnEhZae0wgqLbsK75n 17 | Y2suTTRNqqSuiOaV+HiDFlBf3XjjSVQEF4WCDfKUGzPPNHIDg8B56L1TSELj4+K3 18 | MAt//RyDTc1dmHMmqzK6m2iHyJFcjvQ9GQTk0NcCgYEApmf6aHKUQ8VMd322XYA2 19 | eeveGPRfpd20w32KLiCub6XDEmP5e2Fo/EXOOBhZHMCXR1KqHq7lmULqV0AFvqgr 20 | K8T5b6FdfnBT88BvzhqY5jDKgAR6lrRVSWKGPFWfkq3tUbwSam7sU/b6KbcwcRzt 21 | M8ezgTNyw5WgWGu7Uk9gHSsCgYB7GvNKjaKNhYFwi7NYmUVDdzciI/+YAOQh5wPG 22 | LKqJYnB504zroz9aPK76sCS+ljj/n6aEj9k1NiYrBMQHhyqK6z93r3HbhQbeOZTD 23 | xvWzFVXjEZFvOJ2Qk4B19X/4ViAlhsaTJNkTWbFb2XdDGq9HoEBbpfrq0h9U2mkx 24 | Lag5yQKBgFuANgi+UbQmNbuksiom4Nmy0vb4m7pohR11f/2hyZhrdXgOBiC5v08G 25 | 5rTWa3SaqM1NuXqP22ZaA6RVxtrJI2Jpf5CbSjwxJyHtEKHMkNG3R6FEKL5Axxor 26 | ik0sR5G1Up8fkfPXeeHjXa3cOg9Rpb9da4jsgNxNCthXaDNnZGuu 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "DATA": { 3 | "propertySources": [ 4 | { 5 | "source": { 6 | "key01": "value01", 7 | "key03": null, 8 | "key04.key01": 42 9 | } 10 | }, 11 | { 12 | "source": { 13 | "key01": "banana", 14 | "key02": 2 15 | } 16 | } 17 | ] 18 | }, 19 | "COMPLEX_DATA_1": { 20 | "propertySources": [ 21 | { 22 | "source": { 23 | "key01": "value01", 24 | "key02": null, 25 | "key03.key01[0]": 1, 26 | "key03.key01[1].data": 2, 27 | "key03.key02": 3, 28 | "key04.key01": 42 29 | } 30 | } 31 | ] 32 | }, 33 | "COMPLEX_DATA_2": { 34 | "propertySources": [ 35 | { 36 | "source": { 37 | "data.key01[0][0]": 1, 38 | "data.key01[0][1]": 3, 39 | "data.key01[1][0]": 4, 40 | "data.key01[1][1]": 5 41 | } 42 | } 43 | ] 44 | }, 45 | "SUBSTITUTION": { 46 | "propertySources": [ 47 | { 48 | "source": { 49 | "key01": "Hello", 50 | "key03": 42, 51 | "key04": "${MY_USERNAME}", 52 | "key05": "${MY_USERNAME}-${MY_PASSWORD}", 53 | "key06": false, 54 | "key07": null, 55 | "key08": "${MY_OLD_PASSWORD:super.password}", 56 | "key09": "${MISSING_KEY}" 57 | } 58 | } 59 | ] 60 | }, 61 | "OVERLAPPING_LISTS": { 62 | "propertySources": [ 63 | { 64 | "source": { 65 | "key01[0]": "four", 66 | "key02[0]": 1, 67 | "key02[1]": 2, 68 | "key02[2]": 3, 69 | "key05[0][0]": 100, 70 | "key05[0][1]": 101, 71 | "key05[1][0]": 200 72 | } 73 | }, 74 | { 75 | "source": { 76 | "key01[0]": "one", 77 | "key01[1]": "two", 78 | "key01[2]": "three", 79 | "key02[0]": 99, 80 | "key05[0]": 100, 81 | "key05[1][0]": 90, 82 | "key05[1][1]": 80 83 | } 84 | }, 85 | { 86 | "source": { 87 | "key01[0]": "deepest", 88 | "key02[0]": "deepest", 89 | "key03[0]": 1, 90 | "key03[1]": 2 91 | } 92 | } 93 | ] 94 | } 95 | } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'cloud-config-client' { 4 | 5 | import http = require('http'); 6 | import https = require('https'); 7 | 8 | export function load(options: Options, callback?: LoadCallback): Promise; 9 | 10 | export abstract class Config { 11 | constructor(data: ConfigData, context: { [key: string]: any }); 12 | 13 | properties: { [key: string]: any } 14 | 15 | raw: { [key: string]: any } 16 | 17 | get(keyParts: string): any 18 | 19 | forEach(callback: (property: string, value: string) => void, includeOverridden?: boolean): void 20 | 21 | toObject(): { [key: string]: any } 22 | 23 | toString(spaces: number): string 24 | } 25 | 26 | interface ConfigFile { 27 | /** file name */ 28 | name: string 29 | 30 | /** configuration properties */ 31 | source: ConfigSource 32 | } 33 | 34 | type ConfigSource = { 35 | [key: string]: any 36 | } 37 | 38 | interface ConfigData { 39 | /** application name */ 40 | name: string 41 | 42 | /** ist of profiles included */ 43 | profiles: Array 44 | 45 | /** environment label */ 46 | label: string 47 | 48 | /** commit hash of properties */ 49 | version: string 50 | 51 | /** properties included for application, sorted by more priority */ 52 | propertySources: Array 53 | } 54 | 55 | interface Auth { 56 | /** user id */ 57 | user: string 58 | 59 | /** user password */ 60 | pass: string 61 | } 62 | 63 | interface Options { 64 | /** spring config service url */ 65 | endpoint?: string 66 | 67 | /** if false accepts self-signed certificates */ 68 | rejectUnauthorized?: boolean 69 | 70 | /** @deprecated use name */ 71 | application?: string 72 | 73 | /** application id */ 74 | name: string 75 | 76 | /** application profile(s) */ 77 | profiles?: string|string[] 78 | 79 | /** environment id */ 80 | label?: string 81 | 82 | /** auth configuration */ 83 | auth?: Auth 84 | 85 | /** Agent for the request */ 86 | agent?: http.Agent|https.Agent 87 | 88 | /** Context for substitution */ 89 | context?: { [key: string]: any } 90 | 91 | /** Additional headers */ 92 | headers?: { [key: string]: any } 93 | } 94 | 95 | interface LoadCallback { 96 | (error: Error, config?: Config): void 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Cloud Config Client for NodeJS 2 | 3 | Requires: NodeJS 10+ 4 | 5 | Feature requests are welcome. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm i cloud-config-client 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | const client = require("cloud-config-client"); 17 | client.load({ 18 | application: "invoices" 19 | }).then((config) => { 20 | // Look for a key 21 | const value1 = config.get("this.is.a.key"); 22 | 23 | // Using a prefix, this is equivalent to .get("this.is.another.key"); 24 | const value2 = config.get("this.is", "another.key"); 25 | }); 26 | 27 | // Or 28 | 29 | const config = await client.load({ application: 'invoices' }) 30 | const value1 = config.get("this.is.a.key"); 31 | 32 | ``` 33 | 34 | ### `load` function 35 | 36 | [Load implementation](./index.js) 37 | 38 | #### Parameters 39 | 40 | * options - `Object`, mandatory: 41 | * endpoint `string`, optional, default=`http://localhost:8888`: Config server URL. 42 | * rejectUnauthorized - `boolean`, optional, default = `true`: if `false` accepts self-signed certificates 43 | * application - `string`, **deprecated, use name**: Load configuration for this application. 44 | * name - `string`, mandatory: Load the configuration with this name. 45 | * profiles `string|string[]`, optional, default=`"default"`: Load profiles. 46 | * label - `string`, optional: Load environment. 47 | * auth - `Object`, optional: Basic Authentication for access config server (e.g.: `{ user: "username", pass: "password"}`). 48 | _endpoint_ accepts also basic auth (e.g. `http://user:pass@localhost:8888`). 49 | * user - `string`, mandatory 50 | * pass - `string`, mandatory 51 | * agent - `http.Agent|https.Agent`, optional: Agent for the request. (e.g. use a proxy) (Since version 1.2.0) 52 | * context - `object`, optional: Context for substitution (see context section below) (Since version 1.4.0) 53 | * headers - `object`, optional: Additional request headers (Since version 1.5.0) 54 | * callback - `function(error: Error, config: Config)`, optional: node style callback. If missing, the method will return a promise. 55 | 56 | ```js 57 | client.load(options) 58 | .then((config) => { ... }) 59 | .catch((error) => { ... }) 60 | 61 | // or 62 | 63 | client.load(options, (error, config) => { ... }) 64 | 65 | // or 66 | 67 | async function foo () { 68 | const config = await client.load(options) 69 | //... 70 | } 71 | 72 | ``` 73 | 74 | ### Context references (Since version 1.4.0) 75 | 76 | Strings could contain context references, if a contexts is provided those references will be replaced by the actual values in the `Config` object reference. 77 | 78 | YAML example: 79 | 80 | ```yaml 81 | key01: Hello ${NAME:World}!!! 82 | ``` 83 | 84 | This is not related to spEl and those references are not an expression language. 85 | 86 | Reference structure: `${CONTEXT_KEY:DEFAULT_VALUE?}`: 87 | 88 | * `CONTEXT_KEY`: Context key, if the value for the key is missing it will be use the default value or return the original string 89 | * `DEFAULT_VALUE`: Optional default value 90 | 91 | Environment variables as context: 92 | 93 | ```js 94 | Client.load({ 95 | endpoint: 'http://server:8888', 96 | name: 'application', 97 | context: process.env }) 98 | .then(config => { 99 | // config loaded 100 | }).catch(console.error) 101 | ``` 102 | 103 | ### `Config` object 104 | 105 | [Config class implementation](./lib/config.js) 106 | 107 | #### Properties 108 | 109 | * `raw`: Spring raw response data. 110 | * `properties`: computed properties as per Spring specification: 111 | > Property keys in more specifically named files override those in application.properties or application.yml. 112 | 113 | #### Methods 114 | 115 | * `get(...parts)`: Retrieve a value at a given path or undefined. Multiple parameters can be used to calculate the key. 116 | * parts - `string`, variable, mandatory: 117 | * `forEach(callback, includeOverridden)`: Iterates over every key/value in the config. 118 | * callback - `function(key: string, value: string)`, mandatory: iteration callback. 119 | * includeOverridden - `boolean`, optional, default=`false`: if true, include overridden keys or `replace`. 120 | 121 | ```js 122 | config.get("this.is.a.key"); 123 | config.get("this.is", "a.key"); 124 | config.get("this", "is", "a", "key"); 125 | 126 | config.forEach((key, value) => console.log(key + ":" + value)); 127 | ``` 128 | 129 | * `toString(spaces): string`: Returns a string representation of `raw` property. 130 | * spaces - `number`, optional: spaces to use in format. 131 | * `toObject([options]): object`: Returns the whole configuration as an object. (Since version 1.3.0). 132 | * `options.merge` — specify strategy to process overlapping arrays: 133 | * true — merge overlapping lists 134 | * false — (default) use only the array from the most specific property source: 135 | 136 | ```js 137 | // Simplified data from Spring Cloud Config: 138 | // [ 139 | // {"key01[0]" : 5}, // ← the most specific property source 140 | // {"key01[0]" : "string1", "key01[1]" : "string2"} 141 | // ] 142 | 143 | config.toObject({overlappingArrays:'merge'}) // {"key01[0]": 5, "key01[1]": "string2"} 144 | config.toObject({overlappingArrays:'replace'}) // {"key01[0]": 5} 145 | ``` 146 | 147 | 148 | ## Behind a proxy 149 | 150 | Example (adapted from https-proxy-agent site): 151 | 152 | ```js 153 | const HttpsProxyAgent = require('https-proxy-agent') 154 | const client = require('cloud-config-client') 155 | 156 | const proxy = process.env.http_proxy || 'http://168.63.76.32:3128' 157 | console.log('using proxy server %j', proxy) 158 | const agent = new HttpsProxyAgent(proxy) 159 | 160 | const options = { 161 | application: 'demo', 162 | profiles: ['test', 'timeout'], 163 | agent 164 | } 165 | client.load(options).then((cfg) => { 166 | console.log(cfg.get('test.users', 'multi.uid')) 167 | console.log(cfg.toString(2)) 168 | }).catch((error) => console.error(error)) 169 | ``` 170 | 171 | ## References 172 | 173 | * [Spring Cloud Config](http://cloud.spring.io/spring-cloud-config/) 174 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @module CloudConfigClient 5 | */ 6 | 7 | const http = require('http') 8 | const https = require('https') 9 | // Support node 8 10 | const URL_TYPE = typeof URL === 'undefined' ? require('url').URL : URL 11 | const DEFAULT_URL = new URL_TYPE('http://localhost:8888') 12 | 13 | const Config = require('./lib/config') 14 | 15 | /** 16 | * Client auth configuration 17 | * 18 | * @typedef {Object} Auth 19 | * @property {string} user - user id. 20 | * @property {string} pass - user password. 21 | */ 22 | 23 | /** 24 | * Client configuration 25 | * 26 | * @typedef {Object} Options 27 | * @property {string} [endpoint=http://localhost:8888] - spring config service url 28 | * @property {boolean} [rejectUnauthorized=true] - if false accepts self-signed certificates 29 | * @property {string} [application] - deprecated use name 30 | * @property {string} name - application id 31 | * @property {(string|string[])} [profiles="default"] - application profiles (e.g. ["test", "timeout"] or "test,timeout") 32 | * @property {string} [label] - environment id 33 | * @property {module:CloudConfigClient~Auth} [auth] - auth configuration 34 | * @property {http.Agent|https.Agent} [agent] - Agent for the request. (e.g. use a proxy) (Since version 1.2.0) 35 | * @property {object} [context] - Context for substitution (see context section below) (Since version 1.4.0) 36 | * @property {object} [headers] - Additional headers 37 | */ 38 | 39 | /** 40 | * Handle load response 41 | * 42 | * @callback loadCallback 43 | * @param {?Error} error - whether there was an error retrieving configurations 44 | * @param {module:Config~Config} config - configuration object instance 45 | */ 46 | 47 | /** 48 | * Retrieve basic auth from options 49 | * 50 | * Priority: 51 | * 1. Defined in options 52 | * 2. Coded as basic auth in url 53 | * 54 | * @param {module:CloudConfigClient~Auth} auth - auth configuration. 55 | * @param {URL} url - endpoint. 56 | * @returns {string} basic auth. 57 | */ 58 | function getAuth (auth, url) { 59 | if (auth && auth.user && auth.pass) { 60 | return auth.user + ':' + auth.pass 61 | } 62 | if (url.username || url.password) { 63 | return url.username + ':' + url.password 64 | } 65 | return null 66 | } 67 | 68 | /** 69 | * Build profile string from options value 70 | * 71 | * @param {(string|string[])} [profiles] - list of profiles, if none specified will use 'default' 72 | */ 73 | function buildProfilesString (profiles) { 74 | if (!profiles) { 75 | return 'default' 76 | } 77 | if (Array.isArray(profiles)) { 78 | return profiles.join(',') 79 | } 80 | return profiles 81 | } 82 | 83 | /** 84 | * Build spring config endpoint path 85 | * 86 | * @param {string} path - host base path 87 | * @param {string} name - application name 88 | * @param {(string|string[])} [profiles] - list of profiles, if none specified will use 'default' 89 | * @param {string} [label] - environment id 90 | * @returns {string} spring config endpoint 91 | */ 92 | function getPath (path, name, profiles, label) { 93 | const profilesStr = buildProfilesString(profiles) 94 | return (path.endsWith('/') ? path : path + '/') + 95 | encodeURIComponent(name) + '/' + 96 | encodeURIComponent(profilesStr) + 97 | (label ? '/' + encodeURIComponent(label) : '') 98 | } 99 | 100 | /** 101 | * Load configuration with callback 102 | * 103 | * @param {module:CloudConfigClient~Options} options - spring client configuration options 104 | * @param {module:CloudConfigClient~loadCallback} [callback] - load callback 105 | */ 106 | function loadWithCallback (options, callback) { 107 | const endpoint = options.endpoint ? new URL_TYPE(options.endpoint) : DEFAULT_URL 108 | const name = options.name || options.application 109 | const context = options.context 110 | const client = endpoint.protocol === 'https:' ? https : http 111 | client.request({ 112 | protocol: endpoint.protocol, 113 | hostname: endpoint.hostname, 114 | port: endpoint.port, 115 | path: getPath(endpoint.pathname, name, options.profiles, options.label), 116 | auth: getAuth(options.auth, endpoint), 117 | rejectUnauthorized: options.rejectUnauthorized !== false, 118 | agent: options.agent, 119 | headers: options.headers 120 | }, (res) => { 121 | if (res.statusCode !== 200) { // OK 122 | res.resume() // it consumes response 123 | return callback(new Error('Invalid response: ' + res.statusCode)) 124 | } 125 | let response = '' 126 | res.setEncoding('utf8') 127 | res.on('data', (data) => { 128 | response += data 129 | }) 130 | res.on('end', () => { 131 | try { 132 | const body = JSON.parse(response) 133 | callback(null, new Config(body, context)) 134 | } catch (e) { 135 | callback(e) 136 | } 137 | }) 138 | }).on('error', callback).end() 139 | } 140 | 141 | /** 142 | * Wrap loadWithCallback with Promise 143 | * 144 | * @param {module:CloudConfigClient~Options} options - spring client configuration options 145 | * @returns {Promise} promise handler 146 | */ 147 | function loadWithPromise (options) { 148 | return new Promise((resolve, reject) => { 149 | loadWithCallback(options, (error, config) => { 150 | if (error) { 151 | reject(error) 152 | } else { 153 | resolve(config) 154 | } 155 | }) 156 | }) 157 | } 158 | 159 | module.exports = { 160 | /** 161 | * Retrieve properties from Spring Cloud config service 162 | * 163 | * @param {module:CloudConfigClient~Options} options - spring client configuration options 164 | * @param {module:CloudConfigClient~loadCallback} [callback] - load callback 165 | * @returns {Promise|void} promise handler or void if callback was not defined 166 | * 167 | * @since 1.0.0 168 | */ 169 | load (options, callback) { 170 | return typeof callback === 'function' 171 | ? loadWithCallback(options, callback) 172 | : loadWithPromise(options) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @module Config 5 | */ 6 | 7 | /** 8 | * Retrieve sources from configuration files. 9 | * 10 | * @param {module:Spring~ConfigData} data - spring response data. 11 | * @return {Array} list of sources. 12 | */ 13 | function getSources (data) { 14 | return data.propertySources.map(file => ({ ...file.source })) 15 | } 16 | 17 | function updateCurrent (current, name, indexes, value) { 18 | if (indexes.length) { 19 | if (!current[name]) current[name] = [] 20 | current = current[name] 21 | for (const index of indexes.slice(0, -1)) { 22 | if (!current[index]) current[index] = [] 23 | current = current[index] 24 | } 25 | const last = indexes[indexes.length - 1] 26 | if (!current[last]) current[last] = value 27 | return current[last] 28 | } else if (name) { 29 | if (!current[name]) current[name] = value 30 | return current[name] 31 | } 32 | return current 33 | } 34 | 35 | const keyRE = /^(\S+?)(?:\[\d+\])+$/ 36 | const indexesRE = /\[\d+\]/g 37 | 38 | /** 39 | * Returns a list of keys to be used when convert Config to JS Object 40 | * 41 | * Provide `overlappingArrays` value to set how to process overlapping arrays from different property sources: 42 | * * `merge`(default) — to merge arrays together 43 | * * `replace` — use array from most specific property source 44 | * 45 | * **Example:** 46 | * ``` 47 | * rawData = [ 48 | * {"key01[0]" : 5}, 49 | * {"key01[0]" : "string1", "key01[1]" : "string2"} 50 | * ] 51 | * 52 | * getObjectKeys(rawData, merge = true) // ["key01[0]", "key01[1]"] 53 | * getObjectKeys(rawData) // ["key01[0]"] 54 | * ``` 55 | * 56 | * @param {module:Spring~ConfigData} rawData 57 | * @param {Boolean} merge what strategy to use when processing overlapping arrays from different sources 58 | */ 59 | function getObjectKeys (rawData, merge) { 60 | const sources = getSources(rawData) 61 | 62 | if (!merge) { 63 | const replaceKeys = sources.reverse().map((source) => { 64 | const keys = {} 65 | for (const item in source) { 66 | const match = keyRE.exec(item) 67 | const name = match ? match[1] : item 68 | if (!(name in keys)) { 69 | keys[name] = [] 70 | } 71 | keys[name].push(item) 72 | } 73 | return keys 74 | }) 75 | return ([]).concat(...Object.values(Object.assign(...replaceKeys))) 76 | } 77 | 78 | const uniqueKeysSet = sources.reduce((acc, source) => { 79 | for (const item in source) { 80 | acc.add(item) 81 | } 82 | return acc 83 | }, new Set()) 84 | return Array.from(uniqueKeysSet) 85 | } 86 | 87 | function replacer (ctx) { 88 | return (val) => typeof val === 'string' 89 | ? val.replace( 90 | /\$\{(.+?)(?::(.+?))?\}/g, 91 | (match, group, def) => ctx[group] || def || match) 92 | : val 93 | } 94 | /** 95 | * Configuration handler 96 | * @class 97 | */ 98 | class Config { 99 | /** 100 | * Function to apply to every property in configuration 101 | * 102 | * @callback forEachFunc 103 | * @param {string} key - property key 104 | * @param {*} value - property value 105 | */ 106 | 107 | /** 108 | * Create config object. 109 | * 110 | * @param {module:Spring~ConfigData} data - spring response data 111 | */ 112 | constructor (data, context) { 113 | this._raw = data 114 | const properties = getSources(this._raw).reduce((accum, value) => Object.assign({}, value, accum), {}) 115 | if (context) { 116 | const rep = replacer(context) 117 | this._properties = Object.keys(properties).reduce((acc, key) => { 118 | acc[key] = rep(acc[key]) 119 | return acc 120 | }, properties) 121 | } else { 122 | this._properties = properties 123 | } 124 | } 125 | 126 | /** 127 | * Computed configuration properties 128 | * 129 | * @type {module:Spring~ConfigSource} 130 | * 131 | * @since 1.0.0 132 | */ 133 | get properties () { 134 | return Object.assign({}, this._properties) 135 | } 136 | 137 | /** 138 | * Raw spring response data 139 | * 140 | * @type {module:Spring~ConfigData} 141 | * 142 | * @since 1.0.0 143 | */ 144 | get raw () { 145 | return JSON.parse(JSON.stringify(this._raw)) 146 | } 147 | 148 | /** 149 | * Retrieve a configuration property by key 150 | * 151 | * @param {...string} keyParts - parts to join as key 152 | * @return {*} property value 153 | * 154 | * @since 1.0.0 155 | */ 156 | get (keyParts) { 157 | const key = Array.prototype.slice.call(arguments).join('.') 158 | 159 | return this._properties[key] 160 | } 161 | 162 | /** 163 | * Iterate over configuration properties 164 | * 165 | * @param {module:Config~forEachFunc} callback - function to apply to every property 166 | * @param {boolean} [includeOverridden=false] whether to include overridden properties 167 | * 168 | * @since 1.0.0 169 | */ 170 | forEach (callback, includeOverridden) { 171 | const _include = includeOverridden || false 172 | const sources = _include ? getSources(this._raw) : [this._properties] 173 | 174 | sources.forEach((source) => { 175 | for (const prop in source) { 176 | if (Object.prototype.hasOwnProperty.call(source, prop)) { 177 | callback(prop, source[prop]) 178 | } 179 | } 180 | }) 181 | } 182 | 183 | /** 184 | * Returns the configuration as an object 185 | * 186 | * @param {Object} [options] - configure the conversion process: 187 | * @param {Boolean} options.merge - define strategy to deal with arrays from different 188 | * property sources that have the same key — merge or use the most specific(`replace`) 189 | * 190 | * @return {object} full configuration object 191 | * 192 | * @since 1.3.0 193 | */ 194 | toObject (options = { merge: false }) { 195 | const obj = {} 196 | for (const key of getObjectKeys(this._raw, options.merge)) { 197 | let current = obj 198 | let name = null 199 | let indexes = [] 200 | for (const item of key.split('.')) { 201 | current = updateCurrent(current, name, indexes, {}) 202 | const match = keyRE.exec(item) 203 | if (match) { 204 | name = match[1] 205 | indexes = item.match(indexesRE) 206 | .map(str => Number.parseInt(str.slice(1, -1), 10)) 207 | } else { 208 | name = item 209 | indexes = [] 210 | } 211 | } 212 | updateCurrent(current, name, indexes, this._properties[key]) 213 | } 214 | return obj 215 | } 216 | 217 | /** 218 | * Returns a string representation of raw response data 219 | * 220 | * @param {number} [spaces] - number spaces to use in formatting 221 | * @returns {string} - raw response data as string 222 | * 223 | * @since 1.0.0 224 | */ 225 | toString (spaces) { 226 | return JSON.stringify(this._raw, null, spaces) 227 | } 228 | } 229 | 230 | module.exports = Config 231 | -------------------------------------------------------------------------------- /tests/client.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { describe, it, before, after } = require('mocha') 4 | const Client = require('..') 5 | const { equal, deepEqual, ok, rejects } = require('assert').strict 6 | const AUTH = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=' 7 | const { DATA, COMPLEX_DATA_1, COMPLEX_DATA_2, SUBSTITUTION, OVERLAPPING_LISTS } = 8 | require('./fixtures.json') 9 | 10 | let lastURL = null 11 | let lastHeaders = null 12 | 13 | function basicAssertions (config) { 14 | equal(lastURL, '/application/test%2Ctimeout') 15 | equal(config.get('key01'), 'value01') 16 | equal(config.get('key02'), 2) 17 | equal(config.get('key03'), null) 18 | equal(config.get('missing'), undefined) 19 | equal(config.get('key04.key01'), 42) 20 | equal(config.get('key04', 'key01'), 42) 21 | } 22 | 23 | describe('Spring Cloud Configuration Node Client', function () { 24 | describe('HTTP Server', function () { 25 | const http = require('http') 26 | const port = 15023 27 | const endpoint = 'http://localhost:' + port 28 | let server 29 | 30 | before('start http server', function (done) { 31 | server = http.createServer((req, res) => { 32 | lastURL = req.url 33 | lastHeaders = req.headers 34 | if (lastURL.startsWith('/complex_data1')) { 35 | res.end(JSON.stringify(COMPLEX_DATA_1)) 36 | } else if (lastURL.startsWith('/complex_data2')) { 37 | res.end(JSON.stringify(COMPLEX_DATA_2)) 38 | } else if (lastURL.startsWith('/substitution')) { 39 | res.end(JSON.stringify(SUBSTITUTION)) 40 | } else if (lastURL.startsWith('/overlapping_lists')) { 41 | res.end(JSON.stringify(OVERLAPPING_LISTS)) 42 | } else res.end(JSON.stringify(DATA)) 43 | }).listen(port, done) 44 | server.on('clientError', (err, socket) => { 45 | console.error(err) 46 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n') 47 | }) 48 | }) 49 | 50 | after('stop http server', function (done) { 51 | server.close(done) 52 | }) 53 | 54 | it('supports basic call', async function () { 55 | basicAssertions(await Client.load({ 56 | endpoint, 57 | profiles: ['test', 'timeout'], 58 | name: 'application' 59 | })) 60 | }) 61 | 62 | it('supports provide profiles as a string', async function () { 63 | basicAssertions(await Client.load({ 64 | endpoint, 65 | profiles: 'test,timeout', 66 | name: 'application' 67 | })) 68 | }) 69 | 70 | it('understands application parameter (DEPRECATED)', async function () { 71 | await Client.load({ 72 | endpoint, 73 | profiles: ['test', 'timeout'], 74 | application: 'application' 75 | }) 76 | equal(lastURL, '/application/test%2Ctimeout') 77 | }) 78 | 79 | it('supports explicit auth (auth option)', async function () { 80 | const config = await Client.load({ 81 | endpoint, 82 | name: 'application', 83 | auth: { user: 'username', pass: 'password' } 84 | }) 85 | equal(lastHeaders.authorization, AUTH) 86 | equal(lastURL, '/application/default') 87 | equal(config.get('key02'), 2) 88 | }) 89 | 90 | it('do not send auth header', async function () { 91 | await Client.load({ 92 | endpoint, 93 | name: 'application' 94 | }) 95 | equal(lastHeaders.authorization, undefined) 96 | }) 97 | 98 | it('supports implicit auth (endpoint option)', async function () { 99 | const config = await Client.load({ 100 | endpoint: 'http://username:password@localhost:' + port, 101 | name: 'application' 102 | }) 103 | equal(lastHeaders.authorization, AUTH) 104 | equal(lastURL, '/application/default') 105 | equal(config.get('key02'), 2) 106 | }) 107 | 108 | it('supports label property to get config by environment', async function () { 109 | const config = await Client.load({ 110 | endpoint, 111 | name: 'application', 112 | label: 'develop' 113 | }) 114 | equal(lastURL, '/application/default/develop') 115 | equal(config.get('key02'), 2) 116 | }) 117 | 118 | it('supports custom endpoint path', async function () { 119 | await Client.load({ 120 | endpoint: endpoint + '/justapath', 121 | name: 'mightyapp' 122 | }) 123 | equal(lastURL, '/justapath/mightyapp/default') 124 | }) 125 | 126 | it('returns a config with raw property', async function () { 127 | const { raw } = await Client.load({ 128 | endpoint, 129 | profiles: ['test', 'timeout'], 130 | name: 'application' 131 | }) 132 | deepEqual(raw, DATA) 133 | }) 134 | 135 | describe('CloudConfigClient', function () { 136 | describe('`properties` prop', function () { 137 | it('take key value from more specific propertySources ignoring less specific', async function () { 138 | const config = await Client.load({ 139 | endpoint, 140 | profiles: ['test', 'timeout'], 141 | name: 'application' 142 | }) 143 | 144 | const overriddenKey = 'key01' 145 | const overriddenValues = [] 146 | DATA.propertySources.forEach(({ source }) => { 147 | if (overriddenKey in source) { 148 | overriddenValues.push(source[overriddenKey]) 149 | } 150 | }, true) 151 | 152 | equal(overriddenValues.length, 2) 153 | const expected = { 154 | 'key04.key01': 42, 155 | key01: overriddenValues[0], 156 | key02: 2, 157 | key03: null 158 | } 159 | deepEqual(config.properties, expected) 160 | }) 161 | }) 162 | 163 | describe('`raw` prop', function () { 164 | it('contains raw data from Spring Cloud Config', async function () { 165 | const config = await Client.load({ 166 | endpoint, 167 | profiles: ['test', 'timeout'], 168 | name: 'application' 169 | }) 170 | 171 | deepEqual(config.raw, DATA) 172 | }) 173 | }) 174 | 175 | describe('forEach method', function () { 176 | it('iterates over distinct configuration properties by default', async function () { 177 | const config = await Client.load({ 178 | endpoint, 179 | profiles: ['test', 'timeout'], 180 | name: 'application' 181 | }) 182 | let counter = 0 183 | const uniqueKeys = new Set() 184 | config.forEach((key) => { 185 | uniqueKeys.add(key) 186 | counter++ 187 | }) 188 | 189 | equal(uniqueKeys.size, 4) 190 | equal(counter, 4) 191 | }) 192 | 193 | it('iterates over overridden configuration properties if second param set to `true`', async function () { 194 | const config = await Client.load({ 195 | endpoint, 196 | profiles: ['test', 'timeout'], 197 | name: 'application' 198 | }) 199 | let counter = 0 200 | const uniqueKeys = new Set() 201 | config.forEach((key) => { 202 | uniqueKeys.add(key) 203 | counter++ 204 | }, true) 205 | 206 | equal(uniqueKeys.size, 4) 207 | equal(counter, 5) 208 | }) 209 | }) 210 | 211 | describe('get method', function () { 212 | it('returns value of the given key from the most specific property source', async function () { 213 | const config = await Client.load({ 214 | endpoint, 215 | profiles: ['test', 'timeout'], 216 | name: 'application' 217 | }) 218 | 219 | const overriddenKey = 'key01' 220 | const overriddenValues = [] 221 | DATA.propertySources.forEach(({ source }) => { 222 | if (overriddenKey in source) { 223 | overriddenValues.push(source[overriddenKey]) 224 | } 225 | }, true) 226 | 227 | equal(overriddenValues.length, 2) 228 | // the most specific value goes first in the list of all values of the same key 229 | equal(config.get(overriddenKey), overriddenValues[0]) 230 | equal(config.get('key02'), 2) 231 | }) 232 | }) 233 | 234 | describe('toObject method', function () { 235 | it('responses with JS object with all the unique keys from properties', async function () { 236 | const config = await Client.load({ 237 | endpoint, 238 | profiles: ['test'], 239 | name: 'complex_data1' 240 | }) 241 | deepEqual(config.toObject(), { 242 | key01: 'value01', 243 | key02: null, 244 | key03: { key01: [1, { data: 2 }], key02: 3 }, 245 | key04: { key01: 42 } 246 | }) 247 | }) 248 | 249 | it('processes nested structures correctly', async function () { 250 | const config = await Client.load({ 251 | endpoint, 252 | profiles: ['test'], 253 | name: 'complex_data2' 254 | }) 255 | deepEqual(config.toObject(), { data: { key01: [[1, 3], [4, 5]] } }) 256 | }) 257 | 258 | it('merges arrays if less specific property resource contains same key with bigger array', async function () { 259 | const config = await Client.load({ 260 | endpoint, 261 | profiles: ['test', 'timeout'], 262 | name: 'overlapping_lists' 263 | }) 264 | 265 | const objConfig = config.toObject({ merge: true }) 266 | 267 | deepEqual(objConfig.key01, ['four', 'two', 'three']) 268 | deepEqual(objConfig.key02, [1, 2, 3]) 269 | deepEqual(objConfig.key03, [1, 2]) 270 | deepEqual(objConfig.key05, [[100, 101], [200, 80]]) 271 | }) 272 | 273 | it('replaces less specific arrays if option `merge` is set to `false`', async function () { 274 | const config = await Client.load({ 275 | endpoint, 276 | profiles: ['test', 'timeout'], 277 | name: 'overlapping_lists' 278 | }) 279 | 280 | const objConfig = config.toObject({ merge: false }) 281 | 282 | deepEqual(objConfig.key01, ['four']) 283 | deepEqual(objConfig.key02, [1, 2, 3]) 284 | deepEqual(objConfig.key03, [1, 2]) 285 | deepEqual(objConfig.key05, [[100, 101], [200]]) 286 | 287 | deepEqual(config.toObject(), objConfig) 288 | }) 289 | }) 290 | }) 291 | 292 | it('replaces references with a context object', async function () { 293 | const expectation = { 294 | key01: 'Hello', 295 | key03: 42, 296 | key04: 'Javier', 297 | key05: 'Javier-SecretWord', 298 | key06: false, 299 | key07: null, 300 | key08: 'super.password', 301 | key09: '${MISSING_KEY}' // eslint-disable-line 302 | } 303 | const context = { MY_USERNAME: 'Javier', MY_PASSWORD: 'SecretWord' } 304 | const config = await Client 305 | .load({ endpoint, name: 'substitution', context }) 306 | deepEqual(config.toObject(), expectation) 307 | }) 308 | }) 309 | 310 | describe('HTTPS Server', function () { 311 | const fs = require('fs') 312 | const https = require('https') 313 | const port = 15024 314 | const endpoint = 'https://localhost:' + port 315 | 316 | before('start https server', function (done) { 317 | this.server = https.createServer({ 318 | key: fs.readFileSync('tests/key.pem'), 319 | cert: fs.readFileSync('tests/cert.pem') 320 | }, (req, res) => { 321 | lastURL = req.url 322 | lastHeaders = req.headers 323 | res.end(JSON.stringify(DATA)) 324 | }).listen(port, done) 325 | }) 326 | 327 | after('stop https server', function (done) { 328 | this.server.close(done) 329 | }) 330 | 331 | it('works as expected', async function () { 332 | basicAssertions(await Client.load({ 333 | endpoint, 334 | rejectUnauthorized: false, 335 | profiles: ['test', 'timeout'], 336 | name: 'application' 337 | })) 338 | } 339 | ) 340 | 341 | it('rejects the connection if not authorized', async function () { 342 | await rejects(() => Client.load({ 343 | endpoint, 344 | profiles: ['test', 'timeout'], 345 | name: 'application' 346 | })) 347 | }) 348 | 349 | it('supports calls via proxy agent', async function () { 350 | const agent = new https.Agent() 351 | const old = agent.createConnection.bind(agent) 352 | let used = false 353 | agent.createConnection = function (options, callback) { 354 | used = true 355 | return old(options, callback) 356 | } 357 | const config = await Client.load({ 358 | endpoint, 359 | rejectUnauthorized: false, 360 | profiles: ['test', 'timeout'], 361 | name: 'application', 362 | agent 363 | }) 364 | basicAssertions(config) 365 | ok(used, 'Agent must be used in the call') 366 | agent.destroy() 367 | }) 368 | }) 369 | }) 370 | --------------------------------------------------------------------------------