Skip to content

Commit

Permalink
SNOW-1346233 execute automated external browser tests (#970)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-akolodziejczyk authored Nov 28, 2024
1 parent 753aa3d commit 599e55c
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 189 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/parameters_aws_auth_tests.json.gpg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
� ��P��t�������n4�"���Kx�{3�q��r����*�V}h� I�V_F�-�я[���q��x�/��,�0�N���j���p:k�~�0����;0�fp�tt���QF)q�v5��s٨����>P~TyG;J�b���o��/S����� �u��Na�n�lN���tG����z��-�T�������<S���Nb���jb��Yle�? 9��뼽V�Hӡ<y��
�ƠD[j�sE�?J^�y����u��m_�kL˙�Xv����=zY�b^9�$2Ƌc��eC�]2��!���.����2Y�y��/��H'u�.h��o6��j}ª3��Ib<w�C_r��3�]�g��2���Aݹ�oF
�ԙ����^�
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
*.swp
.idea
.git
parameters.json
parameters*.json
snowflake-sdk-*.tgz
dist
junit*.xml
Expand Down
52 changes: 35 additions & 17 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ timestamps {
stage('Build') {
withCredentials([
usernamePassword(credentialsId: '063fc85b-62a6-4181-9d72-873b43488411', usernameVariable: 'AWS_ACCESS_KEY_ID', passwordVariable: 'AWS_SECRET_ACCESS_KEY'),
string(credentialsId: 'a791118f-a1ea-46cd-b876-56da1b9bc71c',variable: 'NEXUS_PASSWORD')
]) {
string(credentialsId: 'a791118f-a1ea-46cd-b876-56da1b9bc71c', variable: 'NEXUS_PASSWORD')
]) {
sh '''\
|#!/bin/bash -e
|export GIT_BRANCH=${GIT_BRANCH}
Expand All @@ -23,18 +23,36 @@ timestamps {
'''.stripMargin()
}
}
params = [
string(name: 'svn_revision', value: 'main'),
string(name: 'branch', value: 'main'),
string(name: 'client_git_commit', value: scmInfo.GIT_COMMIT),
string(name: 'client_git_branch', value: scmInfo.GIT_BRANCH),
string(name: 'TARGET_DOCKER_TEST_IMAGE', value: 'nodejs-chainguard-node18'),
string(name: 'parent_job', value: env.JOB_NAME),
string(name: 'parent_build_number', value: env.BUILD_NUMBER)
]
stage('Test') {
build job: 'RT-LanguageNodeJS-PC',parameters: params
}

parallel(
'Test': {
stage('Test') {
def params = [
string(name: 'svn_revision', value: 'main'),
string(name: 'branch', value: 'main'),
string(name: 'client_git_commit', value: scmInfo.GIT_COMMIT),
string(name: 'client_git_branch', value: scmInfo.GIT_BRANCH),
string(name: 'TARGET_DOCKER_TEST_IMAGE', value: 'nodejs-chainguard-node18'),
string(name: 'parent_job', value: env.JOB_NAME),
string(name: 'parent_build_number', value: env.BUILD_NUMBER)
]
build job: 'RT-LanguageNodeJS-PC', parameters: params
}
},
'Test Authentication': {
stage('Test Authentication') {
withCredentials([
string(credentialsId: 'a791118f-a1ea-46cd-b876-56da1b9bc71c', variable: 'NEXUS_PASSWORD'),
string(credentialsId: 'sfctest0-parameters-secret', variable: 'PARAMETERS_SECRET')
]) {
sh '''\
|#!/bin/bash -e
|$WORKSPACE/ci/test_authentication.sh
'''.stripMargin()
}
}
}
)
}
}

Expand All @@ -61,7 +79,7 @@ pipeline {
}

def wgetUpdateGithub(String state, String folder, String targetUrl, String seconds) {
def ghURL = "https://api.github.com/repos/snowflakedb/snowflake-connector-nodejs/statuses/$COMMIT_SHA_LONG"
def data = JsonOutput.toJson([state: "${state}", context: "jenkins/${folder}",target_url: "${targetUrl}"])
sh "wget ${ghURL} --spider -q --header='Authorization: token $GIT_PASSWORD' --post-data='${data}'"
def ghURL = "https://api.github.com/repos/snowflakedb/snowflake-connector-nodejs/statuses/$COMMIT_SHA_LONG"
def data = JsonOutput.toJson([state: "${state}", context: "jenkins/${folder}", target_url: "${targetUrl}"])
sh "wget ${ghURL} --spider -q --header='Authorization: token $GIT_PASSWORD' --post-data='${data}'"
}
8 changes: 8 additions & 0 deletions ci/container/test_authentication.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash -e

set -o pipefail

AUTH_PARAMETER_FILE=./.github/workflows/parameters_aws_auth_tests.json
eval $(jq -r '.authtestparams | to_entries | map("export \(.key)=\(.value|tostring)")|.[]' $AUTH_PARAMETER_FILE)

npm run test:authentication
2 changes: 1 addition & 1 deletion ci/container/test_component.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ if [[ -z "$GITHUB_ACTIONS" ]]; then
fi

echo "[INFO] Running Tests: Test result: $WORKSPACE/junit.xml"
if ! ${MOCHA_CMD[@]} "$SOURCE_ROOT/test/**/*.js"; then
if ! ${MOCHA_CMD[@]} 'test/{unit,integration}/**/*.js'; then
echo "[ERROR] Test failed"
[[ -f "$WORKSPACE/junit.xml" ]] && cat $WORKSPACE/junit.xml
exit 1
Expand Down
14 changes: 14 additions & 0 deletions ci/test_authentication.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash -e

set -o pipefail
THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export WORKSPACE=${WORKSPACE:-/tmp}

gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/parameters_aws_auth_tests.json "$THIS_DIR/../.github/workflows/parameters_aws_auth_tests.json.gpg"

docker run \
-v $(cd $THIS_DIR/.. && pwd):/mnt/host \
-v $WORKSPACE:/mnt/workspace \
--rm \
nexus.int.snowflakecomputing.com:8086/docker/snowdrivers-test-external-browser:2 \
"/mnt/host/ci/container/test_authentication.sh"
2 changes: 1 addition & 1 deletion ci/test_windows.bat
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ start /b python hang_webserver.py 12345 > hang_webserver.out 2>&1
popd

echo [INFO] Testing
cmd /c node_modules\.bin\mocha --timeout %TIMEOUT% --recursive --full-trace --color --reporter spec test/**/*.js
cmd /c node_modules\.bin\mocha --timeout %TIMEOUT% --recursive --full-trace --color --reporter spec \"test/{unit,integration}/**/*.js\"
if %ERRORLEVEL% NEQ 0 (
echo [ERROR] failed to run mocha
exit /b 1
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@
"lint:check:all:errorsOnly": "npm run lint:check:all -- --quiet",
"lint:fix": "eslint --fix",
"test": "mocha -timeout 180000 --recursive --full-trace test/unit/**/*.js test/unit/*.js",
"test:authentication": "mocha --exit -timeout 180000 --recursive --full-trace test/authentication/**/*.js test/authentication/*.js",
"test:integration": "mocha -timeout 180000 --recursive --full-trace test/integration/**/*.js test/integration/*.js",
"test:single": "mocha -timeout 180000 --full-trace",
"test:system": "mocha -timeout 180000 --recursive --full-trace system_test/*.js",
"test:unit": "mocha -timeout 180000 --recursive --full-trace test/unit/**/*.js test/unit/*.js",
"test:unit:coverage": "nyc npm run test:unit",
"test:ci": "mocha -timeout 180000 --recursive --full-trace test/**/*.js",
"test:ci": "mocha -timeout 180000 --recursive --full-trace 'test/{unit,integration}/**/*.js'",
"test:ci:coverage": "nyc npm run test:ci",
"test:ci:withSystemTests": "mocha -timeout 180000 --recursive --full-trace test/**/*.js system_test/*.js",
"test:ci:withSystemTests": "mocha -timeout 180000 --recursive --full-trace 'test/{unit,integration}/**/*.js' system_test/*.js",
"test:ci:withSystemTests:coverage": "nyc npm run test:ci:withSystemTests",
"test:manual": "mocha -timeout 180000 --full-trace --full-trace test/integration/testManualConnection.js"
},
Expand Down
30 changes: 30 additions & 0 deletions test/authentication/connectionParameters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const snowflakeAuthTestProtocol = process.env.SNOWFLAKE_AUTH_TEST_PROTOCOL;
const snowflakeAuthTestHost = process.env.SNOWFLAKE_AUTH_TEST_HOST;
const snowflakeAuthTestPort = process.env.SNOWFLAKE_AUTH_TEST_PORT;
const snowflakeAuthTestAccount = process.env.SNOWFLAKE_AUTH_TEST_ACCOUNT;
const snowflakeAuthTestRole = process.env.SNOWFLAKE_AUTH_TEST_ROLE;
const snowflakeTestBrowserUser = process.env.SNOWFLAKE_AUTH_TEST_BROWSER_USER;
const snowflakeAuthTestOktaPass = process.env.SNOWFLAKE_AUTH_TEST_OKTA_PASS;
const snowflakeAuthTestDatabase = process.env.SNOWFLAKE_AUTH_TEST_DATABASE;
const snowflakeAuthTestWarehouse = process.env.SNOWFLAKE_AUTH_TEST_WAREHOUSE;
const snowflakeAuthTestSchema = process.env.SNOWFLAKE_AUTH_TEST_SCHEMA;

const accessUrlAuthTests = snowflakeAuthTestProtocol + '://' + snowflakeAuthTestHost + ':' +
snowflakeAuthTestPort;

const externalBrowser =
{
accessUrl: accessUrlAuthTests,
username: snowflakeTestBrowserUser,
account: snowflakeAuthTestAccount,
role: snowflakeAuthTestRole,
host: snowflakeAuthTestHost,
warehouse: snowflakeAuthTestWarehouse,
database: snowflakeAuthTestDatabase,
schema: snowflakeAuthTestSchema,
authenticator: 'EXTERNALBROWSER'
};

exports.externalBrowser = externalBrowser;
exports.snowflakeTestBrowserUser = snowflakeTestBrowserUser;
exports.snowflakeAuthTestOktaPass = snowflakeAuthTestOktaPass;
203 changes: 203 additions & 0 deletions test/authentication/testExternalBrowser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
const snowflake = require('../../lib/snowflake');
const assert = require('assert');
const testUtil = require('../integration/testUtil');
const connParameters = require('./connectionParameters');
const { spawn } = require('child_process');
const Util = require('../../lib/util');
const JsonCredentialManager = require('../../lib/authentication/secure_storage/json_credential_manager');

describe('External browser authentication tests', function () {
const cleanBrowserProcessesPath = '/externalbrowser/cleanBrowserProcesses.js';
const provideBrowserCredentialsPath = '/externalbrowser/provideBrowserCredentials.js';
const login = connParameters.snowflakeTestBrowserUser;
const password = connParameters.snowflakeAuthTestOktaPass;
let connection, error, callbackCompleted;

before(async () => {
await cleanBrowserProcesses();
});

afterEach(async () => {
await cleanBrowserProcesses();
await destroyConnection(connection);
callbackCompleted = false;
error = undefined;
});

describe('External browser tests', async () => {
it('Successful connection', async () => {
const connectionOption = { ...connParameters.externalBrowser, clientStoreTemporaryCredential: false };
connection = await snowflake.createConnection(connectionOption);
const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000);
await connectAndProvideCredentials(connection, provideCredentialsPromise);
verifyNoErrorWasThrown();
await verifyConnectionIsUp(connection);
});

it('Mismatched Username', async () => {
const connectionOption = { ...connParameters.externalBrowser, username: 'differentUsername', clientStoreTemporaryCredential: false };
connection = await snowflake.createConnection(connectionOption);
const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000);
await connectAndProvideCredentials(connection, provideCredentialsPromise);
assert.strictEqual(error?.message, 'The user you were trying to authenticate as differs from the user currently logged in at the IDP.');
await verifyConnectionIsNotUp(connection, 'Unable to perform operation using terminated connection.');
});

it('Wrong credentials', async () => {
const login = 'itsnotanaccount.com';
const password = 'fakepassword';
const connectionOption = { ...connParameters.externalBrowser, browserActionTimeout: 10000, clientStoreTemporaryCredential: false };
connection = await snowflake.createConnection(connectionOption);
const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'fail', login, password]);
await connectAndProvideCredentials(connection, provideCredentialsPromise);
assert.strictEqual(error?.message, 'Error while getting SAML token: Browser action timed out after 10000 ms.');
await verifyConnectionIsNotUp(connection);
});

it('External browser timeout', async () => {
const connectionOption = { ...connParameters.externalBrowser, browserActionTimeout: 100, clientStoreTemporaryCredential: false };
connection = await snowflake.createConnection(connectionOption);
const connectToBrowserPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'timeout']);
await connectAndProvideCredentials(connection, connectToBrowserPromise);
assert.strictEqual(error?.message, 'Error while getting SAML token: Browser action timed out after 100 ms.');
await verifyConnectionIsNotUp(connection);
});
});

describe('ID Token authentication tests', async () => {
const connectionOption = { ...connParameters.externalBrowser, clientStoreTemporaryCredential: true };
const key = Util.buildCredentialCacheKey(connectionOption.host, connectionOption.username, 'ID_TOKEN');
const defaultCredentialManager = new JsonCredentialManager();
let firstIdToken;

before(async () => {
await defaultCredentialManager.remove(key);
});

it('obtains the id token from the server and saves it on the local storage', async function () {
connection = snowflake.createConnection(connectionOption);
const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000);
await connectAndProvideCredentials(connection, provideCredentialsPromise);
verifyNoErrorWasThrown();
await verifyConnectionIsUp(connection);
});

it('the token is saved in the credential manager', async function () {
firstIdToken = await defaultCredentialManager.read(key);
assert.notStrictEqual(firstIdToken, null);
});

it('authenticates by token, browser credentials not needed', async function () {
connection = snowflake.createConnection(connectionOption);
await connection.connectAsync(connectAsyncCallback());
verifyNoErrorWasThrown();
await verifyConnectionIsUp(connection);
});

it('opens browser okta authentication again when token is incorrect', async function () {
await defaultCredentialManager.write(key, '1234');
connection = snowflake.createConnection(connectionOption);
const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000);
await connectAndProvideCredentials(connection, provideCredentialsPromise);
verifyNoErrorWasThrown();
await verifyConnectionIsUp(connection);
});

it('refreshes the token for credential cache key', async function () {
const newToken = await defaultCredentialManager.read(key);
assert.notStrictEqual(firstIdToken, newToken);
});
});

function connectAsyncCallback() {
return function (err) {
error = err;
callbackCompleted = true;
};
}

function verifyNoErrorWasThrown() {
assert.equal(error, null);
}

async function cleanBrowserProcesses() {
if (process.env.RUN_AUTH_TESTS_MANUALLY !== 'true') {
await execWithTimeout('node', [cleanBrowserProcessesPath], 15000);
}
}

async function connectAndProvideCredentials(connection, provideCredentialsPromise) {
if (process.env.RUN_AUTH_TESTS_MANUALLY === 'true') {
await connection.connectAsync(connectAsyncCallback());
} else {
await Promise.allSettled([connection.connectAsync(connectAsyncCallback()), provideCredentialsPromise]);
}
await waitForCallbackCompletion();
}

async function waitForCallbackCompletion() {
const timeout = Date.now() + 5000;
while (Date.now() < timeout) {
await new Promise(resolve => setTimeout(resolve, 100));
if (callbackCompleted) {
return;
}
}
throw new Error('Connection callback did not complete');
}
});

async function verifyConnectionIsUp(connection) {
assert.ok(await connection.isValidAsync(), 'Connection is not valid');
await testUtil.executeCmdAsync(connection, 'Select 1');
}

async function verifyConnectionIsNotUp(connection, message = 'Unable to perform operation because a connection was never established.') {
assert.ok(!(connection.isUp()), 'Connection should not be up');
try {
await testUtil.executeCmdAsync(connection, 'Select 1');
assert.fail('Expected error was not thrown');
} catch (error) {
assert.strictEqual(error.message, message);
}
}

async function destroyConnection(connection) {
if (connection !== undefined && connection.isUp()) {
await testUtil.destroyConnectionAsync(connection);
}
}

function execWithTimeout(command, args, timeout = 5000) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { shell: true });

let stdout = '';
let stderr = '';

child.stdout.on('data', (data) => {
stdout += data;
});

child.stderr.on('data', (data) => {
stderr += data;
});

child.on('error', (err) => {
reject(err);
});

child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Process exited with code: ${code}, error: ${stderr}`));
} else {
resolve({ stdout, stderr });
}
});

setTimeout(() => {
child.kill();
reject(new Error('Process timed out'));
}, timeout);
});
}
Loading

0 comments on commit 599e55c

Please sign in to comment.