Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-1346233 execute automated external browser tests #970

Merged
merged 8 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
sfc-gh-akolodziejczyk marked this conversation as resolved.
Show resolved Hide resolved
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