diff --git a/.eslintrc.js b/.eslintrc.js index 0030bf492..abc089fe6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,9 +12,15 @@ module.exports = { 'ecmaVersion': 'latest' }, 'rules': { + 'array-bracket-spacing': ['warn'], + 'arrow-spacing': ['warn'], + 'block-spacing': ['warn'], 'brace-style': ['warn', '1tbs'], + 'comma-spacing': ['warn'], 'curly': ['warn', 'all'], 'indent': ['warn', 2], + 'key-spacing': ['warn'], + 'keyword-spacing': ['warn'], 'linebreak-style': ['warn', 'unix'], 'no-async-promise-executor': ['warn'], 'no-console': ['warn', { 'allow': ['warn', 'error'] }], @@ -31,8 +37,16 @@ module.exports = { 'no-useless-catch': ['warn'], 'no-useless-escape': ['warn'], 'no-var': ['warn'], + 'object-curly-spacing': ['warn', 'always'], 'prefer-const': ['warn'], 'quotes': ['warn', 'single'], - 'semi': ['warn', 'always'] + 'semi': ['warn', 'always'], + 'semi-spacing': ['warn'], + 'space-before-function-paren': ['warn', { + 'anonymous': 'always', + 'named': 'never', + 'asyncArrow': 'always', + }], + 'space-infix-ops': ['warn'], } }; diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index eeb8542a2..08a074571 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -114,7 +114,7 @@ jobs: strategy: fail-fast: false matrix: - image: [ 'nodejs-centos7-node14'] + image: [ 'nodejs-centos7-node14', 'nodejs-centos7-fips'] cloud: [ 'AWS', 'AZURE', 'GCP' ] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/snyk-pr.yml b/.github/workflows/snyk-pr.yml index 048b86917..e21815ecf 100644 --- a/.github/workflows/snyk-pr.yml +++ b/.github/workflows/snyk-pr.yml @@ -3,29 +3,36 @@ on: pull_request: branches: - master + +permissions: + contents: read + issues: write + pull-requests: write + jobs: - whitesource: + snyk: + permissions: write-all runs-on: ubuntu-latest if: ${{ github.event.pull_request.user.login == 'sfc-gh-snyk-sca-sa' }} steps: - - name: checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 + - name: checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 - - name: checkout action - uses: actions/checkout@v3 - with: - repository: snowflakedb/whitesource-actions - token: ${{ secrets.WHITESOURCE_ACTION_TOKEN }} - path: whitesource-actions + - name: checkout action + uses: actions/checkout@v3 + with: + repository: snowflakedb/whitesource-actions + token: ${{ secrets.WHITESOURCE_ACTION_TOKEN }} + path: whitesource-actions - - name: PR - uses: ./whitesource-actions/snyk-pr - env: - PR_TITLE: ${{ github.event.pull_request.title }} - with: - jira_token: ${{ secrets.JIRA_TOKEN_PUBLIC_REPO }} - gh_token: ${{ secrets.GITHUB_TOKEN }} - amend: false # true if you want the commit to be amended with the JIRA number + - name: PR + uses: ./whitesource-actions/snyk-pr + env: + PR_TITLE: ${{ github.event.pull_request.title }} + with: + jira_token: ${{ secrets.JIRA_TOKEN_PUBLIC_REPO }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + amend: false # true if you want the commit to be amended with the JIRA number diff --git a/ci/image/Dockerfile.nodejs-centos7-fips-test b/ci/image/Dockerfile.nodejs-centos7-fips-test index 36c9f402c..dce338c21 100644 --- a/ci/image/Dockerfile.nodejs-centos7-fips-test +++ b/ci/image/Dockerfile.nodejs-centos7-fips-test @@ -35,29 +35,49 @@ SHELL [ "/usr/bin/scl", "enable", "devtoolset-8"] # node-fips environment variables ENV NODE_HOME $HOME/node -ENV NODEJS_VERSION 14.0.0 -ENV FIPSDIR $HOME/install-openssl-fips -ENV OPENSSL_VERSION 2.0.16 +ENV NODEJS_VERSION 18.17.0 +ENV OPENSSL_VERSION 3.0.8 +ENV PKG_CONFIG_PATH "/usr/local/lib64/pkgconfig" +ENV LD_LIBRARY_PATH "${LD_LIBRARY_PATH}:/usr/local/lib64" +ENV OPENSSL_CONF /usr/local/ssl/openssl.cnf +ENV FIPSCONF /usr/local/ssl/fipsmodule.cnf +ENV OPENSSL_MODULES=/usr/local/lib64/ossl-modules -# Install OpenSSL +# Install OpenSSL RUN cd $HOME -RUN curl https://www.openssl.org/source/openssl-fips-$OPENSSL_VERSION.tar.gz -o $HOME/openssl-fips-$OPENSSL_VERSION.tar.gz +RUN curl https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz -o $HOME/openssl-fips-$OPENSSL_VERSION.tar.gz RUN tar -xvf $HOME/openssl-fips-$OPENSSL_VERSION.tar.gz -RUN mv openssl-fips-$OPENSSL_VERSION $HOME/openssl-fips +RUN mv openssl-$OPENSSL_VERSION $HOME/openssl-fips RUN cd $HOME/openssl-fips - + +# Install OpenSSL dependencies +RUN yum -y install perl-IPC-Cmd +RUN yum -y install perl-Digest-SHA +RUN yum -y install openssl-devel + # You must run ONLY these commands when building the FIPS version of OpenSSL -RUN cd $HOME/openssl-fips && ./config && make && make install - +RUN cd $HOME/openssl-fips && ./config enable-fips && make && make install + +# Enable FIPS by editing the openssl.cnf file +RUN sed -i "s/openssl_conf = openssl_init/nodejs_conf = openssl_init/g" $OPENSSL_CONF +RUN sed -i "s/# .include fipsmodule.cnf/.include ${FIPSCONF//\//\\/}/g" $OPENSSL_CONF +RUN sed -i 's/# fips = fips_sect/fips = fips_sect/g' $OPENSSL_CONF +RUN sed -i 's/# activate = 1/activate = 1/g' $OPENSSL_CONF +RUN sed -i '55ialg_section = algorithm_sect' $OPENSSL_CONF +RUN sed -i '75idefault_properties = fips=yes' $OPENSSL_CONF +RUN sed -i '75i[algorithm_sect]' $OPENSSL_CONF + # Download and build NodeJS RUN git clone --branch v$NODEJS_VERSION https://github.com/nodejs/node.git $NODE_HOME RUN gcc --version RUN g++ --version -RUN cd $NODE_HOME && ./configure --openssl-fips=$FIPSDIR && make -j2 &> /dev/null && make install +RUN cd $NODE_HOME && ./configure --shared-openssl --shared-openssl-libpath=/usr/local/lib64 --shared-openssl-includes=/usr/local/include/openssl --openssl-is-fips && make -j2 &> /dev/null && make install # Should be $NODEJS_VERSION RUN node --version # Should be $OPENSSL_VERSION RUN node -p "process.versions.openssl" +# Should be 1 (FIPS is enabled by default) +RUN node -p 'crypto.getFips()' # workspace RUN mkdir -p /home/user diff --git a/lib/connection/connection_config.js b/lib/connection/connection_config.js index 3b77cab54..526f95a74 100644 --- a/lib/connection/connection_config.js +++ b/lib/connection/connection_config.js @@ -49,6 +49,7 @@ const DEFAULT_PARAMS = 'arrayBindingThreshold', 'gcsUseDownscopedCredential', 'forceStageBindError', + 'includeRetryReason', 'disableQueryContextCache', ]; @@ -471,7 +472,6 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) let disableQueryContextCache = false; if (Util.exists(options.disableQueryContextCache)) { - Errors.checkArgumentValid(Util.isBoolean(options.disableQueryContextCache), ErrorCodes.ERR_CONN_CREATE_INVALID_DISABLED_QUERY_CONTEXT_CACHE); @@ -494,6 +494,14 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) } } + let includeRetryReason = true; + if (Util.exists(options.includeRetryReason)) { + Errors.checkArgumentValid(Util.isBoolean(options.includeRetryReason), + ErrorCodes.ERR_CONN_CREATE_INVALID_INCLUDE_RETRY_REASON); + + includeRetryReason = options.includeRetryReason; + } + /** * Returns an object that contains information about the proxy hostname, port, * etc. for when http requests are made. @@ -758,6 +766,15 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) }; /** + * Returns whether the Retry reason is included or not in the retry url + * + * @returns {Boolean} + */ + this.getIncludeRetryReason = function () { + return includeRetryReason; + } + + /** * Returns whether the Query Context Cache is enabled or not by the configuration * * @returns {Boolean} diff --git a/lib/connection/result/result.js b/lib/connection/result/result.js index 9d55a0e6c..3534932e0 100644 --- a/lib/connection/result/result.js +++ b/lib/connection/result/result.js @@ -126,7 +126,7 @@ function Result(options) { this._statement, this._services); - this.getQueryContext = function() { + this.getQueryContext = function () { return this._queryContext; } diff --git a/lib/connection/statement.js b/lib/connection/statement.js index dd65640e3..3245fefd4 100644 --- a/lib/connection/statement.js +++ b/lib/connection/statement.js @@ -1403,6 +1403,10 @@ this.sendRequest = function (statementContext, onResultAvailable) json.isInternal = statementContext.internal; } + if(!statementContext.disableQueryContextCache){ + json.queryContextDTO = statementContext.services.sf.getQueryContextDTO(); + } + var options = { method: 'POST', @@ -1576,6 +1580,7 @@ function sendSfRequest(statementContext, options, appendQueryParamOnRetry) var numRetries = 0; var maxNumRetries = connectionConfig.getRetrySfMaxNumRetries(); var sleep = connectionConfig.getRetrySfStartingSleepTime(); + let lastStatusCodeForRetry; // create a function to send the request var sendRequest = function () @@ -1584,7 +1589,14 @@ function sendSfRequest(statementContext, options, appendQueryParamOnRetry) // retry, update the url if ((numRetries > 0) && appendQueryParamOnRetry) { - options.url = Util.url.appendParam(urlOrig, 'retry', true); + const retryOption = { + url: urlOrig, + retryCount: numRetries, + retryReason: lastStatusCodeForRetry, + includeRetryReason: connectionConfig.getIncludeRetryReason(), + } + + options.url = Util.url.appendRetryParam(retryOption); } sf.request(options); @@ -1602,6 +1614,7 @@ function sendSfRequest(statementContext, options, appendQueryParamOnRetry) { // increment the retry count numRetries++; + lastStatusCodeForRetry = err.response ? err.response.statusCode : 0 // use exponential backoff with decorrelated jitter to compute the // next sleep time. diff --git a/lib/constants/error_messages.js b/lib/constants/error_messages.js index 248c3975d..687c189ac 100644 --- a/lib/constants/error_messages.js +++ b/lib/constants/error_messages.js @@ -65,6 +65,7 @@ exports[404038] = 'Invalid gcsUseDownscopedCredential. The specified value must exports[404039] = 'Invalid forceStageBindError. The specified value must be a number.'; exports[404040] = 'Invalid browser timeout value. The specified value must be a positive number.'; exports[404041] = 'Invalid disablQueryContextCache. The specified value must be a boolean.'; +exports[404042] = 'Invalid includeRetryReason. The specified value must be a boolean.' // 405001 exports[405001] = 'Invalid callback. The specified value must be a function.'; diff --git a/lib/errors.js b/lib/errors.js index 5ad788b54..21399bc77 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -70,6 +70,7 @@ codes.ERR_CONN_CREATE_INVALID_GCS_USE_DOWNSCOPED_CREDENTIAL = 404038; codes.ERR_CONN_CREATE_INVALID_FORCE_STAGE_BIND_ERROR = 404039; codes.ERR_CONN_CREATE_INVALID_BROWSER_TIMEOUT = 404040; codes.ERR_CONN_CREATE_INVALID_DISABLED_QUERY_CONTEXT_CACHE = 404041 +codes.ERR_CONN_CREATE_INVALID_INCLUDE_RETRY_REASON =404042 // 405001 codes.ERR_CONN_CONNECT_INVALID_CALLBACK = 405001; diff --git a/lib/queryContextCache.js b/lib/queryContextCache.js index 609bf6266..ef3d1ba44 100644 --- a/lib/queryContextCache.js +++ b/lib/queryContextCache.js @@ -1,6 +1,7 @@ /* - * Copyright (c) 2012-2022 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. */ + const Logger = require('./logger'); /** @@ -10,7 +11,7 @@ const Logger = require('./logger'); * @param {Number} priority * @param {String} context */ -function QueryContextElement (id,timestamp,priority,context) { +function QueryContextElement(id, timestamp, priority, context) { this.id = id; this.timestamp = timestamp; this.priority = priority; @@ -22,11 +23,9 @@ function QueryContextElement (id,timestamp,priority,context) { */ /** - * * @param {Number} capacity Maximum capacity of the cache. */ - -function QueryContextCache (capacity) { +function QueryContextCache(capacity) { this.capacity = capacity; this.idMap = new Map(); // Map for id and QCC this.treeSet = new Set(); // Order data as per priority @@ -34,12 +33,12 @@ function QueryContextCache (capacity) { } QueryContextCache.prototype.sortTreeSet = function () { - this.treeSet = new Set(Array.from(this.treeSet).sort((a,b)=>a.priority-b.priority)); + this.treeSet = new Set(Array.from(this.treeSet).sort((a, b) => a.priority - b.priority)); }; -QueryContextCache.prototype.addQCE= function (qce) { - this.idMap.set(qce.id,qce); - this.priorityMap.set(qce.priority,qce); +QueryContextCache.prototype.addQCE = function (qce) { + this.idMap.set(qce.id, qce); + this.priorityMap.set(qce.priority, qce); this.treeSet.add(qce); this.sortTreeSet(); }; @@ -96,14 +95,12 @@ QueryContextCache.prototype.merge = function (newQCE) { // Change in priority this.replaceQCE(qce, newQCE); } - } - else if (newQCE.timestamp === qce.timestamp && qce.priority !== newQCE.priority) { + } else if (newQCE.timestamp === qce.timestamp && qce.priority !== newQCE.priority) { // Same read timestamp but change in priority this.replaceQCE(qce, newQCE); } - } // id found - else { + } else { // new id if (this.priorityMap.has(newQCE.priority)) { @@ -117,7 +114,7 @@ QueryContextCache.prototype.merge = function (newQCE) { // new priority // Add new element in the cache - this.addQCE(newQCE,newQCE); + this.addQCE(newQCE, newQCE); } } }; @@ -159,7 +156,7 @@ QueryContextCache.prototype.getElements = function () { QueryContextCache.prototype.deserializeQueryContext = function (data) { const stringifyData = JSON.stringify(data); Logger.getInstance().debug(`deserializeQueryContext() called: data from server: ${stringifyData}`); - if (!data || stringifyData ==='{}' || data.entries === null) { + if (!data || stringifyData === '{}' || data.entries === null) { this.clearCache(); Logger.getInstance().debug('deserializeQueryContext() returns'); @@ -217,17 +214,15 @@ QueryContextCache.prototype.deserializeQueryContext = function (data) { }; QueryContextCache.prototype.deserializeQueryContextElement = function (node) { - const {id, timestamp, priority, context} = node; + const { id, timestamp, priority, context } = node; const entry = new QueryContextElement (id, timestamp, priority, null); - if(typeof context === 'string'){ + if (typeof context === 'string'){ entry.context = context; - } - else if (context === null || context === undefined) { + } else if (context === null || context === undefined) { entry.context = null; Logger.getInstance().debug('deserializeQueryContextElement `context` field is empty'); - } - else { + } else { Logger.getInstance().warn('deserializeQueryContextElement: `context` field is not String type'); return null; } @@ -237,23 +232,23 @@ QueryContextCache.prototype.deserializeQueryContextElement = function (node) { QueryContextCache.prototype.logCacheEntries = function () { - this.treeSet.forEach(function(elem) { - Logger.getInstance().debug( - `Cache Entry: id: ${elem.id} timestamp: ${elem.timestamp} priority: ${elem.priority}`, - ); - }); + this.treeSet.forEach(function (elem) { + Logger.getInstance().debug( + `Cache Entry: id: ${elem.id} timestamp: ${elem.timestamp} priority: ${elem.priority}`, + ); + }); }; -QueryContextCache.prototype.getSize = function() { +QueryContextCache.prototype.getSize = function () { return this.treeSet.size; }; QueryContextCache.prototype.getQueryContextDTO = function () { const arr = []; - const querycontexts =Array.from(this.getElements()); + const querycontexts = Array.from(this.getElements()); for (let i = 0; i < this.treeSet.size; i++) { - arr.push({id:querycontexts[i].id, timestamp:querycontexts[i].timestamp, - priority:querycontexts[i].priority, context:{base64Data:querycontexts[i].context}||null}); + arr.push({ id: querycontexts[i].id, timestamp: querycontexts[i].timestamp, + priority: querycontexts[i].priority, context: { base64Data: querycontexts[i].context } || null }); } return { entries: arr @@ -262,9 +257,9 @@ QueryContextCache.prototype.getQueryContextDTO = function () { QueryContextCache.prototype.getSerializeQueryContext = function () { const arr = []; - const querycontexts =Array.from(this.getElements()); + const querycontexts = Array.from(this.getElements()); for (let i = 0; i < this.treeSet.size; i++) { - arr.push({id:querycontexts[i].id,timestamp:querycontexts[i].timestamp,priority:querycontexts[i].priority,context:querycontexts[i].context||null}); + arr.push({ id: querycontexts[i].id, timestamp: querycontexts[i].timestamp, priority: querycontexts[i].priority, context: querycontexts[i].context || null }); } return { diff --git a/lib/util.js b/lib/util.js index 0d607f9e6..8c822b4f1 100644 --- a/lib/util.js +++ b/lib/util.js @@ -344,6 +344,15 @@ exports.url = } return url; + }, + + appendRetryParam: function (option) { + let retryUrl = this.appendParam(option.url, 'retryCount', option.retryCount); + if (option.includeRetryReason) { + retryUrl = this.appendParam(retryUrl, 'retryReason', option.retryReason); + } + + return retryUrl; } }; diff --git a/test/integration/testHTAP.js b/test/integration/testHTAP.js index d5e95c536..c7702ae54 100644 --- a/test/integration/testHTAP.js +++ b/test/integration/testHTAP.js @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + const assert = require('assert'); const async = require('async'); const connOption = require('./connectionOptions').valid; @@ -7,7 +11,7 @@ const Logger = require('../../lib/logger'); if(process.env.CLOUD_PROVIDER === 'AWS') { -describe('Query Context Cache test', function () { + describe('Query Context Cache test', function () { this.timeout(1000000); let connection; diff --git a/test/unit/connection/connection_config_test.js b/test/unit/connection/connection_config_test.js index b8629023c..87e0e9943 100644 --- a/test/unit/connection/connection_config_test.js +++ b/test/unit/connection/connection_config_test.js @@ -1,930 +1,178 @@ /* - * Copyright (c) 2015 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. */ -var ConnectionConfig = require('./../../../lib/connection/connection_config'); -var ErrorCodes = require('./../../../lib/errors').codes; -var assert = require('assert'); +const assert = require('assert'); +const snowflake = require('./../../lib/snowflake'); +const ErrorCodes = require('./../../lib/errors').codes; +const Logger = require('./../../lib/logger'); +const GlobalConfig = require('./../../lib/global_config'); -describe('ConnectionConfig: basic', function () -{ - /////////////////////////////////////////////////////////////////////////// - //// Test synchronous errors //// - /////////////////////////////////////////////////////////////////////////// +const LOG_LEVEL_TAGS = require('./../../lib/logger/core').LOG_LEVEL_TAGS; - var negativeTestCases = - [ - { - name: 'missing options', - options: undefined, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_OPTIONS - }, - { - name: 'null options', - options: null, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_OPTIONS - }, - { - name: 'invalid options', - options: 'invalid', - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_OPTIONS - }, - { - name: 'missing username', - options: {}, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'missing username with SNOWFLAKE authenticator', - options: - { - authenticator: 'SNOWFLAKE' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'missing browser timeout with EXTERNALBROWSER authenticator', - options: - { - authenticator: 'EXTERNALBROWSER', - username: 'admin', - account: 'snowflake', - browserActionTimeout: -1 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_BROWSER_TIMEOUT - }, - { - name: 'missing username with SNOWFLAKE_JWT authenticator', - options: - { - authenticator: 'SNOWFLAKE_JWT' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'undefined username', - options: - { - username: undefined - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'undefined username with SNOWFLAKE authenticator', - options: - { - username: undefined, - authenticator: 'SNOWFLAKE' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'undefined username with SNOWFLAKE_JWT authenticator', - options: - { - username: undefined, - authenticator: 'SNOWFLAKE_JWT' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'null username', - options: - { - username: null - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'null username with SNOWFLAKE authenticator', - options: - { - username: null, - authenticator: 'SNOWFLAKE' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'null username with SNOWFLAKE_JWT authenticator', - options: - { - username: null, - authenticator: 'SNOWFLAKE_JWT' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME - }, - { - name: 'invalid username', - options: - { - username: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_USERNAME - }, - { - name: 'invalid username with SNOWFLAKE authenticator', - options: - { - username: 0, - authenticator: 'SNOWFLAKE' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_USERNAME - }, - { - name: 'invalid username with OAUTH authenticator', - options: - { - username: 0, - authenticator: 'OAUTH' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_USERNAME - }, - { - name: 'invalid username with EXTERNALBROWSER authenticator', - options: - { - username: 0, - authenticator: 'EXTERNALBROWSER' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_USERNAME - }, - { - name: 'invalid username with SNOWFLAKE_JWT authenticator', - options: - { - username: 0, - authenticator: 'SNOWFLAKE_JWT' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_USERNAME - }, - { - name: 'missing password', - options: - { - username: 'username' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_PASSWORD - }, - { - name: 'undefined password', - options: - { - username: 'username', - password: undefined - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_PASSWORD - }, - { - name: 'null password', - options: - { - username: 'username', - password: null - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_PASSWORD - }, - { - name: 'invalid password', - options: - { - username: 'username', - password: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PASSWORD - }, - { - name: 'missing account', - options: - { - username: 'username', - password: 'password' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_ACCOUNT - }, - { - name: 'undefined account', - options: - { - username: 'username', - password: 'password', - account: undefined - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_ACCOUNT - }, - { - name: 'null account', - options: - { - username: 'username', - password: 'password', - account: null - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_ACCOUNT - }, - { - name: 'invalid account', - options: - { - username: 'username', - password: 'password', - account: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_ACCOUNT - }, - { - name: 'invalid warehouse', - options: - { - username: 'username', - password: 'password', - account: 'account', - warehouse: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_WAREHOUSE - }, - { - name: 'invalid database', - options: - { - username: 'username', - password: 'password', - account: 'account', - database: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_DATABASE - }, - { - name: 'invalid schema', - options: - { - username: 'username', - password: 'password', - account: 'account', - schema: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_SCHEMA - }, - { - name: 'invalid role', - options: - { - username: 'username', - password: 'password', - account: 'account', - role: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_ROLE - }, - { - name: 'missing proxyHost', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyPort: '' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_PROXY_HOST - }, - { - name: 'invalid proxyHost', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PROXY_HOST - }, - { - name: 'missing proxyPort', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_PROXY_PORT - }, - { - name: 'invalid proxyPort', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 'proxyPort' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PROXY_PORT - }, - { - name: 'missing proxyUser', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, - proxyPassword: 'proxyPassword' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_PROXY_USER - }, - { - name: 'invalid proxyUser', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, - proxyUser: 1234 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PROXY_USER - }, +describe('Snowflake Configure Tests', function () { + let originalConfig; - { - name: 'missing proxyPassword', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, - proxyUser: 'proxyUser' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_MISSING_PROXY_PASS - }, - { - name: 'invalid proxyPassword', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, - proxyUser: 'proxyUser', - proxyPassword: 1234 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PROXY_PASS - }, - { - name: 'invalid noProxy', - options: - { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, - proxyUser: 'proxyUser', - proxyPassword: 'proxyPassword', - noProxy: 0 - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_NO_PROXY - }, - { - name: 'invalid streamResult', - options: - { - username: 'username', - password: 'password', - account: 'account', - streamResult: 'invalid' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_STREAM_RESULT - }, - { - name: 'invalid fetchAsString', - options: - { - username: 'username', - password: 'password', - account: 'account', - fetchAsString: 'invalid' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_FETCH_AS_STRING - }, - { - name: 'invalid fetchAsString values', - options: - { - username: 'username', - password: 'password', - account: 'account', - fetchAsString: ['invalid'] - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_FETCH_AS_STRING_VALUES - }, - { - name: 'invalid private key value', - options: - { - username: 'username', - password: 'password', - account: 'account', - privateKey: 'abcd', - authenticator: 'SNOWFLAKE_JWT' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PRIVATE_KEY - }, - { - name: 'invalid private key path', - options: - { - username: 'username', - password: 'password', - account: 'account', - privateKeyPath: 1234, - authenticator: 'SNOWFLAKE_JWT' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PRIVATE_KEY_PATH - }, - { - name: 'invalid private key pass', - options: - { - username: 'username', - password: 'password', - account: 'account', - privateKeyPass: 1234, - authenticator: 'SNOWFLAKE_JWT' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PRIVATE_KEY_PASS - }, - { - name: 'invalid oauth token', - options: - { - username: 'username', - password: 'password', - account: 'account', - token: 1234, - authenticator: 'OAUTH' - }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_OAUTH_TOKEN - }, - { - name: 'invalid validateDefaultParameters', - options: + before(function () { + originalConfig = { + logLevel: Logger.getInstance().getLevelTag(), + insecureConnect: GlobalConfig.isInsecureConnect(), + ocspFailOpen: GlobalConfig.getOcspFailOpen(), + jsonColumnVariantParser: GlobalConfig.jsonColumnVariantParser, + xmlColumnVariantParser: GlobalConfig.xmlColumnVariantParser + }; + }); + + after(function () { + snowflake.configure(originalConfig); + }); + + describe('Test invalid arguments', function () { + const negativeTestCases = + [ { - username: 'username', - password: 'password', - account: 'account', - validateDefaultParameters: 2 + name: 'invalid logLevel', + options: { logLevel: 'unsupported' }, + errorCode: ErrorCodes.ERR_GLOBAL_CONFIGURE_INVALID_LOG_LEVEL }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_VALIDATE_DEFAULT_PARAMETERS - }, - { - name: 'invalid application name', - options: { - username: 'username', - password: 'password', - account: 'account', - application: '123Test' + name: 'invalid insecureConnect', + options: { insecureConnect: 'unsupported' }, + errorCode: ErrorCodes.ERR_GLOBAL_CONFIGURE_INVALID_INSECURE_CONNECT }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_APPLICATION - }, - { - name: 'invalid application length', - options: { - username: 'username', - password: 'password', - account: 'account', - application: '0123456789012345678901!%$##234567890123456789012345678901234567890' + name: 'invalid ocspMode', + options: { ocspFailOpen: 'unsupported' }, + errorCode: ErrorCodes.ERR_GLOBAL_CONFIGURE_INVALID_OCSP_MODE }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_APPLICATION - }, - { - name: 'invalid gcsUseDownscopedCredential', - options: { - username: 'username', - password: 'password', - account: 'account', - gcsUseDownscopedCredential: 1234 + name: 'invalid json parser', + options: { jsonColumnVariantParser: 'unsupported' }, + errorCode: ErrorCodes.ERR_GLOBAL_CONFIGURE_INVALID_JSON_PARSER }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_GCS_USE_DOWNSCOPED_CREDENTIAL - }, - { - name: 'invalid disableQueryContextCache', - options: { - username: 'username', - password: 'password', - account: 'account', - disableQueryContextCache: 1234 + name: 'invalid xml parser', + options: { xmlColumnVariantParser: 'unsupported' }, + errorCode: ErrorCodes.ERR_GLOBAL_CONFIGURE_INVALID_XML_PARSER }, - errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_DISABLED_QUERY_CONTEXT_CACHE - }, - ]; - - var createNegativeITCallback = function (testCase) - { - return function () - { - var error; - - try - { - new ConnectionConfig(testCase.options); - } - catch (err) - { - error = err; - } - finally - { - assert.ok(error); - assert.strictEqual(error.code, testCase.errorCode); - } - }; - }; + ]; - var index, length, testCase; - for (index = 0, length = negativeTestCases.length; index < length; index++) - { - testCase = negativeTestCases[index]; - it(testCase.name, createNegativeITCallback(testCase)); - } + negativeTestCases.forEach(testCase => { + it(testCase.name, function () { + let error; - /////////////////////////////////////////////////////////////////////////// - //// Test valid arguments //// - /////////////////////////////////////////////////////////////////////////// + try { + snowflake.configure(testCase.options); + } catch (err) { + error = err; + } finally { + assert.ok(error); + assert.strictEqual(error.code, testCase.errorCode); + } + }); + }); + }); - var testCases = - [ - { - name: 'basic', - input: - { - username: 'username', - password: 'password', - account: 'account' - }, - options: - { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account' - } - }, - { - name: 'region (deprecated)', - input: - { - username: 'username', - password: 'password', - account: 'account', - region: 'testregion', - }, - options: - { - accessUrl: 'https://account.testregion.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account' - } - }, - { - name: 'region in account (deprecated)', - input: - { - username: 'username', - password: 'password', - account: 'account.testregion.azure', - }, - options: - { - accessUrl: 'https://account.testregion.azure.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account' - } - }, - { - name: 'account in url and no account is specified', - input: - { - username: 'username', - password: 'password', - accessUrl: 'https://account1.testregion.azure.snowflakecomputing.com', - }, - options: - { - accessUrl: 'https://account1.testregion.azure.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account1' - } - }, - { - name: 'account in url and account is specified', - input: - { - username: 'username', - password: 'password', - account: 'account2', - accessUrl: 'https://account1.testregion.azure.snowflakecomputing.com', - }, - options: - { - accessUrl: 'https://account1.testregion.azure.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account2' - } - }, - { - name: 'region in account but accessUrl is specified', - input: - { - accessUrl: 'https://account.prodregion.aws.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account.testregion.azure', - }, - options: - { - accessUrl: 'https://account.prodregion.aws.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account' - } - }, - { - name: 'region is us-west-2', - input: - { - username: 'username', - password: 'password', - account: 'account', - region: 'us-west-2', - }, - options: - { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account' - } - }, - { - name: 'region is us-west-2 but account includes us-east-1', - input: - { - username: 'username', - password: 'password', - account: 'account.us-east-1', - region: 'us-west-2', - }, - options: + describe('Test valid arguments', function () { + const testCases = + [ + { + name: 'logLevel error', + options: { - accessUrl: 'https://account.us-east-1.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account' + logLevel: LOG_LEVEL_TAGS.ERROR } - }, - { - name: 'NOT global url', - input: - { - username: 'username', - password: 'password', - account: 'account-123xyz.us-west-2', - }, - options: + }, + { + name: 'logLevel warn', + options: { - accessUrl: 'https://account-123xyz.us-west-2.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account-123xyz' + logLevel: LOG_LEVEL_TAGS.WARN } - }, - { - name: 'global url', - input: - { - username: 'username', - password: 'password', - account: 'account-123xyz.us-west-2.global', - }, - options: + }, + { + name: 'logLevel debug', + options: { - accessUrl: 'https://account-123xyz.us-west-2.global.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account' + logLevel: LOG_LEVEL_TAGS.DEBUG } - }, - { - name: 'validate default parameters', - input: - { - username: 'username', - password: 'password', - account: 'account', - validateDefaultParameters: true }, - options: - { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password', - account: 'account', - } - }, - { - name: 'application', - input: { - username: 'username', - password: 'password', - account: 'account', - application: "test123" + name: 'logLevel info', + options: + { + logLevel: LOG_LEVEL_TAGS.INFO + } }, - options: - { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password' - } - }, - { - name: 'proxy without user/password', - input: { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, + name: 'logLevel trace', + options: + { + logLevel: LOG_LEVEL_TAGS.TRACE + } }, - options: - { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password' - } - }, - { - name: 'proxy with user/password', - input: { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, - proxyUser: 'proxyUser', - proxyPassword: 'proxyPassword' + name: 'insecureConnect false', + options: + { + insecureConnect: false + } }, - options: - { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password' - } - }, - { - name: 'proxy with noproxy', - input: { - username: 'username', - password: 'password', - account: 'account', - proxyHost: 'proxyHost', - proxyPort: 1234, - proxyUser: 'proxyUser', - proxyPassword: 'proxyPassword', - noProxy: '*.snowflakecomputing.com' + name: 'insecureConnect true', + options: + { + insecureConnect: true + } }, - options: { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password' - } - }, - { - name: 'gcsUseDownscopedCredential', - input: - { - username: 'username', - password: 'password', - account: 'account', - gcsUseDownscopedCredential: true + name: 'ocspFailOpen false', + options: + { + ocspFailOpen: false + } }, - options: { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password' - } - }, - { - name: 'oauth without username', - input: - { - account: 'account', - authenticator: 'OAUTH', - token: 'token' + name: 'ocspFailOpen true', + options: + { + ocspFailOpen: true + } }, - options: { - accessUrl: 'https://account.snowflakecomputing.com', - account: 'account' - } - }, - { - name: 'external browser without username and password', - input: - { - account: 'account', - authenticator: 'EXTERNALBROWSER' + name: 'json parser', + options: + { + jsonColumnVariantParser: rawColumnValue => require('vm').runInNewContext('(' + rawColumnValue + ')') + } }, - options: { - accessUrl: 'https://account.snowflakecomputing.com', - account: 'account' - } - }, - { - name: 'disableQueryContextCache', - input: - { - username: 'username', - password: 'password', - account: 'account', - disableQueryContextCache: true + name: 'xml parser', + options: + { + xmlColumnVariantParser: rawColumnValue => new (require("fast-xml-parser")).XMLParser().parse(rawColumnValue) + } }, - options: - { - accessUrl: 'https://account.snowflakecomputing.com', - username: 'username', - password: 'password' - } - }, - ]; - - var createItCallback = function (testCase) - { - return function () - { - var result_options = new ConnectionConfig(testCase.input); - Object.keys(testCase.options).forEach(function (key) - { - var ref = testCase.options[key]; - var val = result_options[key]; - assert.strictEqual(val, ref); - }) - }; - }; - - for (index = 0, length = testCases.length; index < length; index++) - { - testCase = testCases[index]; - it(testCase.name, createItCallback(testCase)); - } - - it('custom prefetch', function () - { - var username = 'username'; - var password = 'password'; - var account = 'account'; - - var connectionConfig = new ConnectionConfig( - { - username: username, - password: password, - account: account - }); - - // get the default value of the resultPrefetch parameter - var resultPrefetchDefault = connectionConfig.getResultPrefetch(); - - // create a ConnectionConfig object with a custom value for resultPrefetch - var resultPrefetchCustom = resultPrefetchDefault + 1; - connectionConfig = new ConnectionConfig( - { - username: username, - password: password, - account: account, - resultPrefetch: resultPrefetchCustom + ]; + + testCases.forEach(testCase => { + it(testCase.name, function () { + snowflake.configure(testCase.options); + Object.keys(testCase.options).forEach(function (key) { + const ref = testCase.options[key]; + let val; + if (key == 'logLevel') { + val = Logger.getInstance().getLevelTag(); + } else if (key == 'insecureConnect') { + val = GlobalConfig.isInsecureConnect(); + } else if (key == 'ocspFailOpen') { + val = GlobalConfig.getOcspFailOpen(); + } else { + val = GlobalConfig[key]; + } + assert.strictEqual(val, ref); + }); }); - - // verify that the custom value overrode the default value - assert.strictEqual( - connectionConfig.getResultPrefetch(), resultPrefetchCustom); + }); }); }); diff --git a/test/unit/query_context_cache_test.js b/test/unit/query_context_cache_test.js index 5d9cea884..139af8f95 100644 --- a/test/unit/query_context_cache_test.js +++ b/test/unit/query_context_cache_test.js @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + const QueryContextCache = require('../../lib/queryContextCache.js'); const assert = require('assert'); @@ -80,7 +84,6 @@ function TestingQCC () { describe('Query Context Cache Test', function () { const testingQcc = new TestingQCC(); - /** Test for empty cache */ it('test - the cache is empty',function () { testingQcc.initCache(); assert.strictEqual(testingQcc.qcc.getSize(), 0); @@ -105,7 +108,7 @@ describe('Query Context Cache Test', function () { // Add one more element at the end const i = MAX_CAPACITY; - const extraQCE = new QueryContextElement(BASE_ID + i, BASE_READ_TIMESTAMP + i, BASE_PRIORITY + i, CONTEXT) + const extraQCE = new QueryContextElement(BASE_ID + i, BASE_READ_TIMESTAMP + i, BASE_PRIORITY + i, CONTEXT); testingQcc.qcc.merge(extraQCE); testingQcc.qcc.checkCacheCapacity(); @@ -134,7 +137,7 @@ describe('Query Context Cache Test', function () { const updatedID = 3; const updatedPriority = BASE_PRIORITY + updatedID + 7; testingQcc.expectedPriority[updatedID] = updatedPriority; - const updatedQCE = new QueryContextElement(BASE_ID + updatedID, BASE_READ_TIMESTAMP + updatedID, testingQcc.expectedPriority[updatedID], CONTEXT) + const updatedQCE = new QueryContextElement(BASE_ID + updatedID, BASE_READ_TIMESTAMP + updatedID, testingQcc.expectedPriority[updatedID], CONTEXT); testingQcc.qcc.merge(updatedQCE); testingQcc.qcc.checkCacheCapacity(); @@ -170,7 +173,7 @@ describe('Query Context Cache Test', function () { // Add one more element with same priority const i = 2; - const samePriorityQCE = new QueryContextElement(BASE_ID + i, BASE_READ_TIMESTAMP + i - 10, BASE_PRIORITY + i, CONTEXT) + const samePriorityQCE = new QueryContextElement(BASE_ID + i, BASE_READ_TIMESTAMP + i - 10, BASE_PRIORITY + i, CONTEXT); testingQcc.qcc.merge(samePriorityQCE); testingQcc.qcc.checkCacheCapacity(); diff --git a/test/unit/util_test.js b/test/unit/util_test.js index 485938297..6a0e11e57 100644 --- a/test/unit/util_test.js +++ b/test/unit/util_test.js @@ -488,6 +488,40 @@ describe('Util', function () } }); + describe('Append retry parameters', function () { + const testCases = + [ + { + testName: "test appending retry params with retry reason", + option: { + url: 'http://www.something.snowflakecomputing.com', + retryCount: 3, + retryReason: 429, + includeRetryReason: true, + }, + result: 'http://www.something.snowflakecomputing.com?retryCount=3&retryReason=429' + }, + { + testName: "test appending retry params without retry reason", + option: { + url: 'http://www.something.snowflakecomputing.com', + retryCount: 3, + retryReason: 429, + includeRetryReason: false, + }, + result: 'http://www.something.snowflakecomputing.com?retryCount=3' + } + ]; + + for (let i = 0; i < testCases.length; i++) { + const testCase = testCases[i]; + it(testCase.testName, function () { + const url = Util.url.appendRetryParam(testCase.option); + assert.strictEqual(url, testCase.result); + }) + } + }) + it('Util.apply()', function () { assert.strictEqual(Util.apply(null, null), null);