diff --git a/.editorconfig b/.editorconfig index 00778972e..59b148394 100755 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,4 @@ indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true +indent_size = 2 \ No newline at end of file diff --git a/README.md b/README.md index 4dbb95f1f..760b4ea4d 100644 --- a/README.md +++ b/README.md @@ -497,6 +497,59 @@ For example, if you are using GitHub and you would like to give the Broker acces More details can be found here: [Detecting infrastructure as code files using a broker](https://docs.snyk.io/products/snyk-infrastructure-as-code/detecting-infrastructure-as-code-files-using-a-broker) +### Credential Pooling +Under some circumstances it can be desirable to create a "pool" of credentials, e.g., to work around rate-limiting issues. +This can be achieved by creating an environment variable ending in `_ARRAY`, separate each credential with a comma, and +the Broker Client will then, when doing variable replacement, look to see if the variable in use has a variant with an +`_ARRAY` suffix, and use the next item in that array if so. For example, if you have set the environment variable +`GITHUB_TOKEN`, but want to provide multiple tokens, you would just do this: + +```shell +GITHUB_TOKEN_ARRAY=token1, token2, token3 +``` + +And then the Broker Server would, any time it needed `GITHUB_TOKEN`, instead take an item from the `GITHUB_TOKEN_ARRAY`. + +Credentials will be taken in a round-robin fashion, so the first, the second, the third, etc, etc, until it reaches the end +and then takes the first one again. + +Calling the `/systemcheck` endpoint will validate all credentials, in order, and will return an array where the first item +is the first credential and so on. For example, if you were running the GitHub Client and had this: + +```shell +GITHUB_TOKEN_ARRAY=good_token, bad_token +``` + +The `/systemcheck` endpoint would return the following, where the first object is for `good_token` and the second for +`bad_token`: + +```json +[ + { + "brokerClientValidationUrl": "https://api.github.com/user", + "brokerClientValidationMethod": "GET", + "brokerClientValidationTimeoutMs": 5000, + "ok": true + }, + { + "brokerClientValidationUrl": "https://api.github.com/user", + "brokerClientValidationMethod": "GET", + "brokerClientValidationTimeoutMs": 5000, + "ok": false, + "error": "401 - {\"message\":\"Bad credentials\",\"documentation_url\":\"https://docs.github.com/rest\"}" + } +] +``` +The actual credentials are not included to avoid exposing sensitive data accidentally. + +#### Limitations +Credential validity is not checked before using a credential, nor are invalid credentials removed from the pool, so it is +_strongly_ recommended that credentials be used exclusively by the Broker Client to avoid credentials reaching rate limits +at different times, and that the `/systemcheck` endpoint be called before use. + +Some providers, such as GitHub, do rate-limiting on a per-user basis, not a per-token or per-credential basis, and in those +cases you will need to create multiple accounts with one credential per account. + ### Custom approved-listing filter The default approved-listing filter supports the bare minimum to operate on all repositories supported by Snyk. In order to customize the approved-listing filter, create the default one locally by installing `snyk-broker` and running `broker init [Git type]`. The created `accept.json` is the default filter for the chosen Git. Place the file in a separate folder such as `./private/accept.json`, and provide it to the docker container by mounting the folder and using the `ACCEPT` environment variable: diff --git a/lib/client/index.js b/lib/client/index.js index db5d11ff1..c3d7d58ec 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1,12 +1,12 @@ const primus = require('primus'); -const request = require('request'); +const rp = require('request-promise-native'); const socket = require('./socket'); const relay = require('../relay'); const logger = require('../log'); const version = require('../version'); -module.exports = ({ port = null, config = {}, filters = {} }) => { - logger.info({ version }, 'running in client mode'); +module.exports = ({port = null, config = {}, filters = {}}) => { + logger.info({version}, 'running in client mode'); const identifyingMetadata = { version, @@ -22,7 +22,7 @@ module.exports = ({ port = null, config = {}, filters = {} }) => { }); // start the local webserver to listen for relay requests - const { app, server } = require('../webserver')(config, port); + const {app, server} = require('../webserver')(config, port); // IMPORTANT: defined before relay (`app.all('/*', ...`) app.get(config.brokerHealthcheckPath || '/healthcheck', (req, res) => { @@ -41,7 +41,7 @@ module.exports = ({ port = null, config = {}, filters = {} }) => { return res.status(status).json(data); }); - app.get(config.brokerSystemcheckPath || '/systemcheck', (req, res) => { + app.get(config.brokerSystemcheckPath || '/systemcheck', async (req, res) => { // Systemcheck is the broker client's ability to assert the network // reachability and some correctness of credentials for the service // being proxied by the broker client. @@ -52,74 +52,42 @@ module.exports = ({ port = null, config = {}, filters = {} }) => { config.brokerClientValidationTimeoutMs || 5000; const isJsonResponse = !config.brokerClientValidationJsonDisabled; - const data = { - brokerClientValidationUrl: logger.sanitise( - config.brokerClientValidationUrl, - ), - brokerClientValidationMethod, - brokerClientValidationTimeoutMs, - }; - - const validationRequestHeaders = { - 'user-agent': 'Snyk Broker client ' + version, - }; - // set auth header according to config - if (config.brokerClientValidationAuthorizationHeader) { - validationRequestHeaders.authorization = - config.brokerClientValidationAuthorizationHeader; + let auths = []; + if (config.brokerClientValidationAuthorizationHeaderArray) { + auths = config.brokerClientValidationAuthorizationHeaderArray; + } else if (config.brokerClientValidationBasicAuthArray) { + auths = config.brokerClientValidationBasicAuthArray.map(s => `Basic ${Buffer.from(s).toString('base64')}`) + } else if (config.brokerClientValidationAuthorizationHeader) { + auths.push(config.brokerClientValidationAuthorizationHeader); } else if (config.brokerClientValidationBasicAuth) { - validationRequestHeaders.authorization = `Basic ${Buffer.from( - config.brokerClientValidationBasicAuth, - ).toString('base64')}`; + auths.push(`Basic ${Buffer.from(config.brokerClientValidationBasicAuth).toString('base64')}`) } // make the internal validation request - request( - { - url: config.brokerClientValidationUrl, - headers: validationRequestHeaders, - method: brokerClientValidationMethod, - timeout: brokerClientValidationTimeoutMs, - json: isJsonResponse, - agentOptions: { - ca: config.caCert, // Optional CA cert - }, - }, - (error, response) => { - // test logic requires to surface internal data - // which is best not exposed in production - if (process.env.TAP) { - data.testError = error; - data.testResponse = response; - } - - if (error) { - data.ok = false; - data.error = error.message; - return res.status(500).json(data); - } - - const responseStatusCode = response && response.statusCode; - data.brokerClientValidationUrlStatusCode = responseStatusCode; - - // check for 2xx status code - const goodStatusCode = /^2/.test(responseStatusCode); - if (!goodStatusCode) { - data.ok = false; - data.error = - responseStatusCode === 401 || responseStatusCode === 403 - ? 'Failed due to invalid credentials' - : 'Status code is not 2xx'; - - logger.error(data, response && response.body, 'Systemcheck failed'); - return res.status(500).json(data); - } + const rv = []; + let errorOccurred = true; + if (auths.length > 0) { + for (let i = 0; i < auths.length; i++) { + logger.info(`Checking if credentials at index ${i} are valid`) + const auth = auths[i]; + let [data, err] = await checkCredentials(auth, config, brokerClientValidationMethod, brokerClientValidationTimeoutMs, isJsonResponse) + rv.push(data) + errorOccurred = err; + logger.info("Credentials checked") + } + } else { + logger.info("No credentials specified - checking if target can be accessed without credentials") + let [data, err] = await checkCredentials(null, config, brokerClientValidationMethod, brokerClientValidationTimeoutMs, isJsonResponse) + rv.push(data) + errorOccurred = err; + } - data.ok = true; - return res.status(200).json(data); - }, - ); + if (errorOccurred) { + return res.status(500).json(rv); + } else { + return res.status(200).json(rv); + } }); // relay all other URL paths @@ -146,3 +114,72 @@ module.exports = ({ port = null, config = {}, filters = {} }) => { }, }; }; + +async function checkCredentials(auth, config, brokerClientValidationMethod, brokerClientValidationTimeoutMs, isJsonResponse) { + const data = { + brokerClientValidationUrl: logger.sanitise( + config.brokerClientValidationUrl, + ), + brokerClientValidationMethod, + brokerClientValidationTimeoutMs, + }; + + const validationRequestHeaders = { + 'user-agent': 'Snyk Broker client ' + version, + }; + if (auth) { + validationRequestHeaders.authorization = auth; + } + + let errorOccurred = true; + // This was originally `request`, but `await` is a lot easier to understand than nested callback hell. + await rp( + { + url: config.brokerClientValidationUrl, + headers: validationRequestHeaders, + method: brokerClientValidationMethod, + timeout: brokerClientValidationTimeoutMs, + json: isJsonResponse, + resolveWithFullResponse: true, + agentOptions: { + ca: config.caCert, // Optional CA cert + }, + }, + ).then(response => { + // test logic requires to surface internal data + // which is best not exposed in production + if (process.env.TAP) { + data.testResponse = response; + } + + const responseStatusCode = response && response.statusCode; + data.brokerClientValidationUrlStatusCode = responseStatusCode; + + // check for 2xx status code + const goodStatusCode = /^2/.test(responseStatusCode); + if (!goodStatusCode) { + data.ok = false; + data.error = + responseStatusCode === 401 || responseStatusCode === 403 + ? 'Failed due to invalid credentials' + : 'Status code is not 2xx'; + + logger.error(data, response && response.body, 'Systemcheck failed'); + return; + } + + errorOccurred = false; + data.ok = true; + }).catch(error => { + // test logic requires to surface internal data + // which is best not exposed in production + if (process.env.TAP) { + data.testError = error; + } + + data.ok = false; + data.error = error.message; + }); + + return [data, errorOccurred]; +} diff --git a/lib/config.js b/lib/config.js index 4abf61b8e..208bcf78d 100644 --- a/lib/config.js +++ b/lib/config.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const camelcase = require('camelcase'); -const { loadConfig } = require('snyk-config'); +const {loadConfig} = require('snyk-config'); const config = loadConfig(__dirname + '/..'); function camelify(res) { @@ -14,14 +14,51 @@ function camelify(res) { } function expandValue(obj, value) { - return value.replace(/([\\]?\$.+?\b)/g, (all, key) => { - if (key[0] === '$') { - const keyToReplace = key.slice(1); - return obj[keyToReplace] || key; + let arrayFound = undefined; + let keyWithArray = undefined; + const variableRegex = /(\\?\$.+?\b)/g; + const variableMatcher = value.match(variableRegex); + if (variableMatcher) { + for (const key of variableMatcher) { + if (key[0] === "$" && obj[(key.slice(1) + "_ARRAY")]) { + keyWithArray = key.slice(1); + arrayFound = (key.slice(1) + "_ARRAY"); + break; + } } + } + + if (arrayFound) { + const values = []; + let array; + if (Array.isArray(obj[arrayFound])) { + array = obj[arrayFound]; + } else { + array = obj[arrayFound].split(",").map(s => s.trim()); + obj[arrayFound] = array; + } + + for (const o of array) { + values.push(value.replace(variableRegex, (all, key) => { + if (key[0] === '$') { + const keyToReplace = key.slice(1); + return keyToReplace === keyWithArray ? o : (obj[keyToReplace] || key); + } + + return key; + })); + } + return values; + } else { + return value.replace(variableRegex, (all, key) => { + if (key[0] === '$') { + const keyToReplace = key.slice(1); + return obj[keyToReplace] || key; + } - return key; - }); + return key; + }); + } } function expand(obj) { @@ -29,7 +66,10 @@ function expand(obj) { for (const key of keys) { const value = expandValue(obj, obj[key]); - if (value !== obj[key]) { + if (value && Array.isArray(value)) { + // This will get camel-cased later on + obj[(key + "_ARRAY")] = value; + } else if (value !== obj[key]) { obj[key] = value; } } @@ -53,4 +93,10 @@ if (res.caCert) { res.caCert = fs.readFileSync(path.resolve(process.cwd(), res.caCert)); } +for (const [key, value] of Object.entries(res)) { + if ((key.endsWith("Array") || key.endsWith("_ARRAY")) && !Array.isArray(value)) { + res[key] = value.split(",").map(s => s.trim()); + } +} + module.exports = res; diff --git a/lib/replace-vars.js b/lib/replace-vars.js index 529b69e6a..838ab24c2 100644 --- a/lib/replace-vars.js +++ b/lib/replace-vars.js @@ -5,8 +5,28 @@ module.exports = { function replace(input, source) { return (input || '').replace(/(\${.*?})/g, (_, match) => { - const key = match.slice(2, -1); // ditch the wrappers - return source[key] || ''; + const key = match.slice(2, -1); // ditch the wrappers + let keyName; + let idxName; + if (source[(key + "_ARRAY")]) { + keyName = key + "_ARRAY"; + idxName = key + "_ARRAY_IDX"; + } else if (source[(key + "Array")]) { + keyName = key + "Array"; + idxName = key + "ArrayIdx"; + } + + const array = source[keyName]; + let idx; + if (array) { + idx = source[idxName] || 0; + if (idx >= array.length) { + idx = 0; + } + source[idxName] = idx + 1; + } + + return array ? array[idx] : source[key] || ''; }); } diff --git a/package.json b/package.json index 38851619b..9b24585bc 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "prom-client": "^11.5.3", "qs": "^6.9.1", "request": "^2.88.1", + "request-promise-native": "^1.0.9", "snyk-config": "^4.0.0-rc.2", "then-fs": "^2.0.0", "tunnel": "0.0.6", diff --git a/test/functional/server-client-pooled-credentials.test.js b/test/functional/server-client-pooled-credentials.test.js new file mode 100644 index 000000000..252caaf99 --- /dev/null +++ b/test/functional/server-client-pooled-credentials.test.js @@ -0,0 +1,137 @@ +const test = require('tap-only'); +const path = require('path'); +const request = require('request'); +const app = require('../../lib'); +const version = require('../../lib/version'); +const root = __dirname; + +const { port, createTestServer } = require('../utils'); + +test('proxy requests originating from behind the broker server with pooled credentials', (t) => { + /** + * 1. start broker in server mode + * 2. start broker in client mode and join (1) + * 3. run local http server that replicates "private server" + * 4. send requests to **server** + * + * Note: client is forwarding requests to echo-server defined in test/util.js + */ + + const { echoServerPort, testServer } = createTestServer(); + + t.teardown(() => { + testServer.close(); + }); + + const ACCEPT = 'filters.json'; + process.env.ACCEPT = ACCEPT; + + process.chdir(path.resolve(root, '../fixtures/server')); + process.env.BROKER_TYPE = 'server'; + const serverPort = port(); + const server = app.main({ port: serverPort }); + + const clientRootPath = path.resolve(root, '../fixtures/client'); + process.chdir(clientRootPath); + const BROKER_SERVER_URL = `http://localhost:${serverPort}`; + const BROKER_TOKEN = '98f04768-50d3-46fa-817a-9ee6631e9970'; + process.env.BROKER_TYPE = 'client'; + process.env.GITHUB = 'github.com'; + process.env.BROKER_TOKEN = BROKER_TOKEN; + process.env.BROKER_SERVER_URL = BROKER_SERVER_URL; + process.env.ORIGIN_PORT = echoServerPort; + process.env.USERNAME = 'user@email.com'; + process.env.PASSWORD = 'not-used'; + process.env.PASSWORD1 = 'aB}#/:%40*1'; + process.env.PASSWORD2 = 'aB}#/:%40*2'; + process.env.PASSWORD_ARRAY = '$PASSWORD1, $PASSWORD2'; + const client = app.main({ port: port() }); + + // wait for the client to successfully connect to the server and identify itself + server.io.on('connection', (socket) => { + socket.on('identify', (clientData) => { + const token = clientData.token; + t.plan(5); + + t.test('identification', (t) => { + const filters = require(`${clientRootPath}/${ACCEPT}`); + t.equal(clientData.token, BROKER_TOKEN, 'correct token'); + t.deepEqual( + clientData.metadata, + { + version, + filters, + }, + 'correct metadata', + ); + t.end(); + }); + + t.test( + 'successfully broker on endpoint that forwards requests with basic auth, using first credential', + (t) => { + const url = `http://localhost:${serverPort}/broker/${token}/basic-auth`; + request({ url, method: 'get' }, (err, res) => { + t.equal(res.statusCode, 200, '200 statusCode [1]'); + + const auth = res.body.replace('Basic ', ''); + const encodedAuth = Buffer.from(auth, 'base64').toString('utf-8'); + t.equal( + encodedAuth, + `${process.env.USERNAME}:${process.env.PASSWORD1}`, + 'auth header is set correctly [1]', + ); + t.end(); + }); + }, + ); + + t.test( + 'successfully broker on endpoint that forwards requests with basic auth, using second credential', + (t) => { + const url = `http://localhost:${serverPort}/broker/${token}/basic-auth`; + request({ url, method: 'get' }, (err, res) => { + t.equal(res.statusCode, 200, '200 statusCode [2]'); + + const auth = res.body.replace('Basic ', ''); + const encodedAuth = Buffer.from(auth, 'base64').toString('utf-8'); + t.equal( + encodedAuth, + `${process.env.USERNAME}:${process.env.PASSWORD2}`, + 'auth header is set correctly [2]', + ); + t.end(); + }); + }, + ); + + t.test( + 'successfully broker on endpoint that forwards requests with basic auth, using first credential again', + (t) => { + const url = `http://localhost:${serverPort}/broker/${token}/basic-auth`; + request({ url, method: 'get' }, (err, res) => { + t.equal(res.statusCode, 200, '200 statusCode [3]'); + + const auth = res.body.replace('Basic ', ''); + const encodedAuth = Buffer.from(auth, 'base64').toString('utf-8'); + t.equal( + encodedAuth, + `${process.env.USERNAME}:${process.env.PASSWORD1}`, + 'auth header is set correctly [3]', + ); + t.end(); + }); + }, + ); + + t.test('clean up', (t) => { + client.close(); + setTimeout(() => { + server.close(); + t.ok('sockets closed'); + t.end(); + }, 100); + }); + }); + }); +}); diff --git a/test/functional/systemcheck.test.js b/test/functional/systemcheck.test.js index f99c1be1d..9ceb6126a 100644 --- a/test/functional/systemcheck.test.js +++ b/test/functional/systemcheck.test.js @@ -26,18 +26,18 @@ test('broker client systemcheck endpoint', (t) => { process.chdir(path.resolve(root, '../fixtures/client')); const clientPort = port(); - t.plan(4); + t.plan(5); const clientUrl = `http://localhost:${clientPort}`; - t.test('good validation url, custom endpoint', (t) => { + t.test('good validation url, custom endpoint, no authorization', (t) => { const client = app.main({ port: clientPort, config: { brokerType: 'client', brokerToken: '1234567890', brokerServerUrl: 'http://localhost:12345', - brokerClientValidationUrl: 'https://snyk.io', + brokerClientValidationUrl: 'https://httpbin.org/headers', brokerSystemcheckPath: '/custom-systemcheck', }, }); @@ -50,12 +50,13 @@ test('broker client systemcheck endpoint', (t) => { } t.equal(res.statusCode, 200, '200 statusCode'); - t.equal(res.body.ok, true, '{ ok: true } in body'); + t.equal(res.body[0].ok, true, '[{ ok: true }] in body'); t.equal( - res.body.brokerClientValidationUrl, - 'https://snyk.io', + res.body[0].brokerClientValidationUrl, + 'https://httpbin.org/headers', 'validation url present', ); + t.equal(res.body[0].testResponse.body.headers.Authorization, undefined, 'does not have authorization header'); client.close(); setTimeout(() => { @@ -84,18 +85,18 @@ test('broker client systemcheck endpoint', (t) => { } t.equal(res.statusCode, 200, '200 statusCode'); - t.equal(res.body.ok, true, '{ ok: true } in body'); + t.equal(res.body[0].ok, true, '[{ ok: true }] in body'); t.equal( - res.body.brokerClientValidationUrl, + res.body[0].brokerClientValidationUrl, 'https://httpbin.org/headers', 'validation url present', ); t.ok( - res.body.testResponse.body.headers['User-Agent'], + res.body[0].testResponse.body.headers['User-Agent'], 'user-agent header is present in validation request', ); t.equal( - res.body.testResponse.body.headers.Authorization, + res.body[0].testResponse.body.headers.Authorization, 'token my-special-access-token', 'proper authorization header in validation request', ); @@ -125,21 +126,21 @@ test('broker client systemcheck endpoint', (t) => { } t.equal(res.statusCode, 200, '200 statusCode'); - t.equal(res.body.ok, true, '{ ok: true } in body'); + t.equal(res.body[0].ok, true, '[{ ok: true }] in body'); t.equal( - res.body.brokerClientValidationUrl, + res.body[0].brokerClientValidationUrl, 'https://httpbin.org/headers', 'validation url present', ); t.ok( - res.body.testResponse.body.headers['User-Agent'], + res.body[0].testResponse.body.headers['User-Agent'], 'user-agent header is present in validation request', ); const expectedAuthHeader = `Basic ${Buffer.from( 'username:password', ).toString('base64')}`; t.equal( - res.body.testResponse.body.headers.Authorization, + res.body[0].testResponse.body.headers.Authorization, expectedAuthHeader, 'proper authorization header in request', ); @@ -151,6 +152,66 @@ test('broker client systemcheck endpoint', (t) => { }); }); + t.test('good validation url, basic auth, both good', (t) => { + const client = app.main({ + port: clientPort, + config: { + brokerType: 'client', + brokerToken: '1234567890', + brokerServerUrl: 'http://localhost:12345', + brokerClientValidationUrl: 'https://httpbin.org/headers', + brokerClientValidationBasicAuthArray: ['username:password', 'username1:password1'], + }, + }); + + request({ url: `${clientUrl}/systemcheck`, json: true }, (err, res) => { + if (err) { + return t.threw(err); + } + + t.equal(res.statusCode, 200, '200 statusCode'); + t.equal(res.body[0].ok, true, '[{ ok: true }, ...] in body'); + t.equal(res.body[1].ok, true, '[..., { ok: true }] in body'); + t.equal( + res.body[0].brokerClientValidationUrl, + 'https://httpbin.org/headers', + 'validation url present [0]', + ); + t.equal( + res.body[1].brokerClientValidationUrl, + 'https://httpbin.org/headers', + 'validation url present [1]', + ); + t.ok( + res.body[0].testResponse.body.headers['User-Agent'], + 'user-agent header is present in validation request [0]', + ); + t.ok( + res.body[1].testResponse.body.headers['User-Agent'], + 'user-agent header is present in validation request [1]', + ); + t.equal( + res.body[0].testResponse.body.headers.Authorization, + `Basic ${Buffer.from( + 'username:password', + ).toString('base64')}`, + 'proper authorization header in request [0]', + ); + t.equal( + res.body[1].testResponse.body.headers.Authorization, + `Basic ${Buffer.from( + 'username1:password1', + ).toString('base64')}`, + 'proper authorization header in request [1]', + ); + + client.close(); + setTimeout(() => { + t.end(); + }, 100); + }); + }); + t.test('bad validation url', (t) => { const client = app.main({ port: clientPort, @@ -168,9 +229,9 @@ test('broker client systemcheck endpoint', (t) => { } t.equal(res.statusCode, 500, '500 statusCode'); - t.equal(res.body.ok, false, '{ ok: false } in body'); + t.equal(res.body[0].ok, false, '[{ ok: false }] in body'); t.equal( - res.body.brokerClientValidationUrl, + res.body[0].brokerClientValidationUrl, 'https://snyk.io/no-such-url-ever', 'validation url present', ); diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index a5d717247..162af5127 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -2,7 +2,14 @@ describe('config', () => { it('contain application config', () => { const foo = (process.env.FOO = 'bar'); const token = (process.env.BROKER_TOKEN = '1234'); + const bitbucketTokens = ['1234', '5678']; + const githubTokens = ['9012', '3456']; + process.env.BITBUCKET_PASSWORD_ARRAY = '1234, 5678' + process.env.GITHUB_TOKEN_ARRAY = '9012, 3456' process.env.FOO_BAR = '$FOO/bar'; + process.env.GITHUB_USERNAME = 'git' + process.env.GITHUB_PASSWORD_ARRAY = '9012, 3456' + process.env.GITHUB_AUTH = 'Basic $GITHUB_USERNAME:$GITHUB_PASSWORD'; const complexToken = (process.env.COMPLEX_TOKEN = '1234$$%#@!$!$@$$$'); const config = require('../../lib/config'); @@ -11,5 +18,11 @@ describe('config', () => { expect(config.brokerToken).toEqual(token); expect(config.fooBar).toEqual('bar/bar'); expect(config.complexToken).toEqual(complexToken); + expect(config.bitbucketPasswordArray).toEqual(bitbucketTokens); + expect(config.BITBUCKET_PASSWORD_ARRAY).toEqual(bitbucketTokens); + expect(config.githubTokenArray).toEqual(githubTokens); + expect(config.GITHUB_TOKEN_ARRAY).toEqual(githubTokens); + expect(config.githubAuthArray).toEqual(['Basic git:9012', 'Basic git:3456']); + expect(config.GITHUB_AUTH_ARRAY).toEqual(['Basic git:9012', 'Basic git:3456']); }); }); diff --git a/test/unit/replace-vars.test.ts b/test/unit/replace-vars.test.ts index 3f39cffa4..eea08c88c 100644 --- a/test/unit/replace-vars.test.ts +++ b/test/unit/replace-vars.test.ts @@ -1,4 +1,4 @@ -import { replaceUrlPartialChunk } from '../../lib/replace-vars'; +import {replace, replaceUrlPartialChunk} from '../../lib/replace-vars'; describe('replacePartialChunk', () => { const config = { @@ -54,3 +54,40 @@ describe('replacePartialChunk', () => { }); }); }); + +describe('replace - with arrays', () => { + const config = { + RES_BODY_URL_SUB: 'http://replac.ed', + BROKER_SERVER_URL: 'broker.com', + BROKER_TOKEN: 'a-tok-en', + BITBUCKET_PASSWORD_ARRAY: ['1', '2', '3'], + GITHUB_TOKEN_ARRAY: ['1'], + githubTokenArray: ['1'], + }; + + it('Uses an array if configured - upper case', () => { + const chunk = 'START ${GITHUB_TOKEN} END'; + const expected = 'START 1 END'; + + expect(replace(chunk, config)).toEqual(expected); + }); + + it('Uses an array if configured - camel case', () => { + const chunk = 'START ${githubToken} END'; + const expected = 'START 1 END'; + + expect(replace(chunk, config)).toEqual(expected); + }); + + it('Goes back to the start of the array if end reached', () => { + const chunk = 'START ${BITBUCKET_PASSWORD} END'; + + expect(replace(chunk, config)).toEqual('START 1 END'); + + expect(replace(chunk, config)).toEqual('START 2 END'); + + expect(replace(chunk, config)).toEqual('START 3 END'); + + expect(replace(chunk, config)).toEqual('START 1 END'); + }); +})