1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-08 04:50:06 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Yury Shkoda
59195e6379 Merge branch 'master' into snyk-upgrade-f3e7f573ad5222a75980dcffc4381458 2022-05-10 12:27:32 +03:00
snyk-bot
3a820c56a9 fix: upgrade @sasjs/utils from 2.42.0 to 2.42.1
Snyk has created this PR to upgrade @sasjs/utils from 2.42.0 to 2.42.1.

See this package in npm:
https://www.npmjs.com/package/@sasjs/utils

See this project in Snyk:
https://app.snyk.io/org/allanbowe/project/2cf085e5-c256-4a84-bf6a-227076754853?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-04-23 23:21:18 +00:00
29 changed files with 4335 additions and 13591 deletions

View File

@@ -1,12 +0,0 @@
#!/bin/sh
# Using `--silent` helps for showing any errs in the first line of the response
# The first line is picked up by the VS Code GIT UI popup when rc is not 0
if npm run --silent lint:silent ; then
exit 0
else
npm run --silent lint:fix
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
exit 1
fi

View File

@@ -1,30 +0,0 @@
cipher AES-256-CBC
setenv FORWARD_COMPATIBLE 1
client
server-poll-timeout 4
nobind
remote vpn.analytium.co.uk 1194 udp
remote vpn.analytium.co.uk 1194 udp
remote vpn.analytium.co.uk 443 tcp
remote vpn.analytium.co.uk 1194 udp
remote vpn.analytium.co.uk 1194 udp
remote vpn.analytium.co.uk 1194 udp
remote vpn.analytium.co.uk 1194 udp
remote vpn.analytium.co.uk 1194 udp
dev tun
dev-type tun
ns-cert-type server
setenv opt tls-version-min 1.0 or-highest
reneg-sec 604800
sndbuf 0
rcvbuf 0
# NOTE: LZO commands are pushed by the Access Server at connect time.
# NOTE: The below line doesn't disable LZO.
comp-lzo no
verb 3
setenv PUSH_PEER_INFO
ca ca.crt
cert user.crt
key user.key
tls-auth tls.key 1

View File

@@ -4,6 +4,7 @@
name: SASjs Build name: SASjs Build
on: on:
push:
pull_request: pull_request:
jobs: jobs:
@@ -21,77 +22,19 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- name: Check npm audit - name: Check npm audit
run: npm audit --production --audit-level=low run: npm audit --production --audit-level=low
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Check code style - name: Check code style
run: npm run lint run: npm run lint
- name: Run unit tests - name: Run unit tests
run: npm test run: npm test
- name: Build Package
run: npm run package:lib
env:
CI: true
- name: Install SSH Key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.DCGITLAB_KEY }}
known_hosts: 'placeholder'
- name: Write VPN Files
run: |
echo "$CA_CRT" > .github/vpn/ca.crt
echo "$USER_CRT" > .github/vpn/user.crt
echo "$USER_KEY" > .github/vpn/user.key
echo "$TLS_KEY" > .github/vpn/tls.key
shell: bash
env:
CA_CRT: ${{ secrets.CA_CRT}}
USER_CRT: ${{ secrets.USER_CRT }}
USER_KEY: ${{ secrets.USER_KEY }}
TLS_KEY: ${{ secrets.TLS_KEY }}
- name: Install Open VPN
run: |
sudo apt install apt-transport-https
sudo wget https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub
sudo apt-key add openvpn-repo-pkg-key.pub
sudo wget -O /etc/apt/sources.list.d/openvpn3.list https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-focal.list
sudo apt update
sudo apt install openvpn3=16~beta+focal
- name: Start Open VPN 3
run: openvpn3 session-start --config .github/vpn/config.ovpn
- name: Deploy sasjs-tests
run: |
npm install -g replace-in-files-cli
cd sasjs-tests
replace-in-files --regex='"@sasjs/adapter".*' --replacement='"@sasjs/adapter":"latest",' ./package.json
npm i
replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./public/config.json
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
npm run update:adapter && npm run build
scp -o stricthostkeychecking=no -r ./build/* ${{ secrets.DCGITLAB_DEPLOY_PATH_VIYA }}
- name: Run cypress on sasjs
run: |
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"${{ secrets.SASJS_TEST_URL_VIYA }}",' ./cypress.json
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./cypress.json
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
sh ./sasjs-cypress-run.sh ${{ secrets.DISCORD_WEBHOOK }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
# For some reason if coverage report action is run before other commands, those commands can't access the directories and files on which they depend on
- name: Generate coverage report - name: Generate coverage report
uses: artiomtr/jest-coverage-report-action@v2.0-rc.2 uses: artiomtr/jest-coverage-report-action@v2.0-rc.2
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Package
run: npm run package:lib
env:
CI: true

View File

@@ -14,16 +14,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Check code style - name: Check code style
run: npm run lint run: npm run lint
- name: Build Project - name: Build Project
run: npm run build run: npm run build
- name: Semantic Release - name: Semantic Release
uses: cycjimmy/semantic-release-action@v2 uses: cycjimmy/semantic-release-action@v2
env: env:

View File

@@ -4,13 +4,3 @@ docs/
*.md *.md
*.spec.ts *.spec.ts
.all-contributorsrc .all-contributorsrc
cypress/
.gitpod.yml
.prettierrc
cypress.json
jest.config.js
sasjs-cypress-run.sh
tsconfig.json
tslint.json
typedoc.json
webpack.config.js

View File

@@ -16,5 +16,5 @@ No PR (that involves a non-trivial code change) should be merged, unless all ite
- [ ] All `sasjs-cli` unit tests are passing (`npm test`). - [ ] All `sasjs-cli` unit tests are passing (`npm test`).
- (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)). - [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya - [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya

View File

@@ -1,11 +0,0 @@
{
"chromeWebSecurity": false,
"defaultCommandTimeout": 20000,
"env": {
"sasjsTestsUrl": "",
"username": "",
"password": "",
"screenshotOnRunFailure": false,
"testingFinishTimeout": 600000
}
}

View File

@@ -1,78 +0,0 @@
const sasjsTestsUrl = Cypress.env('sasjsTestsUrl')
const username = Cypress.env('username')
const password = Cypress.env('password')
const testingFinishTimeout = Cypress.env('testingFinishTimeout')
context('sasjs-tests', function () {
this.beforeAll(() => {
cy.visit(sasjsTestsUrl)
})
this.beforeEach(() => {
cy.reload()
})
it('Should have all tests successfull', (done) => {
cy.get('body').then(($body) => {
if ($body.find('input[placeholder="User Name"]').length > 0) {
cy.get('input[placeholder="User Name"]').type(username)
cy.get('input[placeholder="Password"]').type(password)
cy.get('.submit-button').click()
}
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
.should('not.exist')
.then(() => {
cy.get('.ui.massive.icon.primary.left.labeled.button')
.click()
.then(() => {
cy.get('.ui.massive.loading.primary.button', {
timeout: testingFinishTimeout
})
.should('not.exist')
.then(() => {
cy.get('span.icon.failed')
.should('not.exist')
.then(() => {
done()
})
})
})
})
})
})
it('Should have all tests successfull with debug on', (done) => {
cy.get('body').then(($body) => {
if ($body.find('input[placeholder="User Name"]').length > 0) {
cy.get('input[placeholder="User Name"]').type(username)
cy.get('input[placeholder="Password"]').type(password)
cy.get('.submit-button').click()
}
cy.get('.ui.fitted.toggle.checkbox label')
.click()
.then(() => {
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
.should('not.exist')
.then(() => {
cy.get('.ui.massive.icon.primary.left.labeled.button')
.click()
.then(() => {
cy.get('.ui.massive.loading.primary.button', {
timeout: testingFinishTimeout
})
.should('not.exist')
.then(() => {
cy.get('span.icon.failed')
.should('not.exist')
.then(() => {
done()
})
})
})
})
})
})
})
})

View File

@@ -1,31 +0,0 @@
const wp = require('@cypress/webpack-preprocessor')
const webpackOptions = {
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
loaders: ['ts-loader'],
exclude: [/node_modules/]
},
{
test: /\.(html|css)$/,
loader: 'raw-loader',
exclude: /\.async\.(html|css)$/
},
{
test: /\.async\.(html|css)$/,
loaders: ['file?name=[name].[hash].[ext]', 'extract']
}
]
}
}
const options = {
webpackOptions
}
module.exports = wp(options)

View File

@@ -1,42 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const wp = require('@cypress/webpack-preprocessor')
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
const options = {
webpackOptions: require('../webpack.config.js')
}
on('file:preprocessor', wp(options))
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-site-isolation-trials')
launchOptions.args.push('--auto-open-devtools-for-tabs')
launchOptions.args.push('--aggressive-cache-discard')
launchOptions.args.push('--disable-cache')
launchOptions.args.push('--disable-application-cache')
launchOptions.args.push('--disable-offline-load-stale-cache')
launchOptions.args.push('--disk-cache-size=0')
return launchOptions
}
})
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es6",
"lib": ["es2019", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}

View File

@@ -1,23 +0,0 @@
module.exports = {
mode: 'development',
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: [/node_modules/],
use: [
{
loader: 'ts-loader',
options: {
// skip typechecking for speed
transpileOnly: true
}
}
]
}
]
}
}

9540
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,17 +8,14 @@
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node", "build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack", "package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack",
"publish:lib": "npm run build && cd build && npm publish", "publish:lib": "npm run build && cd build && npm publish",
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"", "lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"", "lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint:silent": "npx prettier --loglevel silent --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"test": "jest --silent --coverage", "test": "jest --silent --coverage",
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build", "prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
"postpublish": "git clean -fd", "postpublish": "git clean -fd",
"semantic-release": "semantic-release", "semantic-release": "semantic-release",
"typedoc": "node createTSDocs", "typedoc": "node createTSDocs",
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true", "prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true"
"cypress": "cypress open",
"cy:run": "cypress run"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
@@ -43,13 +40,9 @@
}, },
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@cypress/webpack-preprocessor": "5.9.1",
"@types/form-data": "2.5.0",
"cypress": "7.7.0",
"typedoc-neo-theme": "1.1.1",
"typedoc-plugin-external-module-name": "4.0.6",
"@types/axios": "0.14.0", "@types/axios": "0.14.0",
"@types/express": "4.17.13", "@types/express": "4.17.13",
"@types/form-data": "2.5.0",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/mime": "2.0.3", "@types/mime": "2.0.3",
"@types/pem": "1.9.6", "@types/pem": "1.9.6",

View File

@@ -1,10 +0,0 @@
#!/bin/bash
if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
echo "Cypress sasjs testing passed!"
else
curl -X POST --header "Content-Type:application/json" --data '{"username":"GitHub CI - Adapter SASJS-TESTS (FAIL)", "content":"Automated sasjs-tests failed on the @sasjs/adapter PR on following link.\n'$2'", "avatar_url":"https://i.ibb.co/Lpk7Xvq/error-outline.png"}' $1
echo "Cypress sasjs testing failed!"
exit 1
fi

View File

@@ -60,7 +60,7 @@ If you'd like to deploy just `sasjs-tests` without changing the adapter version,
The below services need to be created on your SAS server, at the location specified as the `appLoc` in the SASjs configuration. The below services need to be created on your SAS server, at the location specified as the `appLoc` in the SASjs configuration.
The code below will work on ALL SAS platforms (Viya, SAS 9 EBI, SASjs Server). ### SAS 9
```sas ```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
@@ -70,32 +70,20 @@ parmcards4;
%webout(FETCH) %webout(FETCH)
%webout(OPEN) %webout(OPEN)
%macro x(); %macro x();
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables)); %do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
%let table=%scan(&sasjs_tables,&i);
%webout(OBJ,&table,missing=STRING,showmeta=YES)
%end;
%else %do i=1 %to &_webin_file_count;
%webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES)
%end;
%mend; %x() %mend; %x()
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mx_createwebservice(path=/Public/app/common,name=sendObj) %mm_createwebservice(path=/Public/app/common,name=sendObj)
parmcards4; parmcards4;
%webout(FETCH) %webout(FETCH)
%webout(OPEN) %webout(OPEN)
%macro x(); %macro x();
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables)); %do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
%let table=%scan(&sasjs_tables,&i);
%webout(ARR,&table,missing=STRING,showmeta=YES)
%end;
%else %do i=1 %to &_webin_file_count;
%webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES)
%end;
%mend; %x() %mend; %x()
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mx_createwebservice(path=/Public/app/common,name=sendArr) %mm_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4; parmcards4;
data work.macvars; data work.macvars;
set sashelp.vmacro; set sashelp.vmacro;
@@ -104,14 +92,11 @@ parmcards4;
%webout(OBJ,macvars) %webout(OBJ,macvars)
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mx_createwebservice(path=/Public/app/common,name=sendMacVars) %mm_createwebservice(path=/Public/app/common,name=sendMacVars)
parmcards4; parmcards4;
If you can keep your head when all about you let he who hath understanding, reckon the number of the beast
Are losing theirs and blaming it on you,
If you can trust yourself when all men doubt you,
But make allowance for their doubting too;
;;;; ;;;;
%mx_createwebservice(path=/Public/app/common,name=makeErr) %mm_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4; parmcards4;
%webout(OPEN) %webout(OPEN)
data _null_; data _null_;
@@ -120,7 +105,66 @@ data _null_;
run; run;
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mx_createwebservice(path=/Public/app/common,name=invalidJSON) %mm_createwebservice(path=/Public/app/common,name=invalidJSON)
```
### SAS Viya
```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp lrecl=1000;
parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i);
%webout(OBJ,&table,missing=STRING,showmeta=YES)
%end;
%mend;
%x()
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendObj)
parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i);
%webout(ARR,&table,missing=STRING,showmeta=YES)
%end;
%mend;
%x()
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4;
data work.macvars;
set sashelp.vmacro;
run;
%webout(OPEN)
%webout(OBJ,macvars)
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendMacVars)
parmcards4;
If you can keep your head when all about you
Are losing theirs and blaming it on you,
If you can trust yourself when all men doubt you,
But make allowance for their doubting too;
;;;;
%mp_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4;
%webout(OPEN)
data _null_;
file _webout;
put ' the discovery channel ';
run;
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=invalidJSON)
``` ```
You should now be able to access the tests in your browser at the deployed path on your server. You should now be able to access the tests in your browser at the deployed path on your server.

File diff suppressed because it is too large Load Diff

View File

@@ -61,7 +61,7 @@ export const basicTests = (
'Should fail on first attempt and should log the user in on second attempt', 'Should fail on first attempt and should log the user in on second attempt',
test: async () => { test: async () => {
await adapter.logOut() await adapter.logOut()
await adapter.logIn('invalid', 'invalid').catch((err: any) => {}) await adapter.logIn('invalid', 'invalid')
return await adapter.logIn(userName, password) return await adapter.logIn(userName, password)
}, },
assertion: (response: any) => assertion: (response: any) =>
@@ -161,17 +161,26 @@ export const basicTests = (
} }
}, },
{ {
title: 'Web request', title: 'Request with extra attributes on JES approach',
description: 'Should run the request with old web approach', description:
'Should complete successful request with extra attributes present in response',
test: async () => { test: async () => {
const config: Partial<SASjsConfig> = { const config: Partial<SASjsConfig> = {
useComputeApi: false useComputeApi: false
} }
return await adapter.request('common/sendArr', stringData, config) return await adapter.request(
'common/sendArr',
stringData,
config,
undefined,
undefined,
['file', 'data']
)
}, },
assertion: (response: any) => { assertion: (response: any) => {
return response.table1[0][0] === stringData.table1[0].col1 const responseKeys: any = Object.keys(response)
return responseKeys.includes('file') && responseKeys.includes('data')
} }
} }
] ]

View File

@@ -1,35 +1,9 @@
import SASjs from '@sasjs/adapter' import SASjs from '@sasjs/adapter'
import { TestSuite } from '@sasjs/test-framework' import { TestSuite } from '@sasjs/test-framework'
const stringData: any = { table1: [{ col1: 'first col value' }] }
export const computeTests = (adapter: SASjs): TestSuite => ({ export const computeTests = (adapter: SASjs): TestSuite => ({
name: 'Compute', name: 'Compute',
tests: [ tests: [
{
title: 'Compute API request',
description: 'Should run the request with compute API approach',
test: async () => {
return await adapter.request('common/sendArr', stringData)
},
assertion: (response: any) => {
return response.table1[0][0] === stringData.table1[0].col1
}
},
{
title: 'JES API request',
description: 'Should run the request with JES API approach',
test: async () => {
const config = {
useComputeApi: false
}
return await adapter.request('common/sendArr', stringData, config)
},
assertion: (response: any) => {
return response.table1[0][0] === stringData.table1[0].col1
}
},
{ {
title: 'Start Compute Job - not waiting for result', title: 'Start Compute Job - not waiting for result',
description: 'Should start a compute job and return the session', description: 'Should start a compute job and return the session',

View File

@@ -138,7 +138,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
result = result =
result && result &&
res.table1[index][3] === res.table1[index][3] ===
(multipleRowsWithNulls.table1[index].col4 || '') (multipleRowsWithNulls.table1[index].col4 || ' ')
}) })
return result return result
} }
@@ -164,7 +164,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
result = result =
result && result &&
res.table1[index][3] === res.table1[index][3] ===
(multipleColumnsWithNulls.table1[index].col4 || '') (multipleColumnsWithNulls.table1[index].col4 || ' ')
}) })
return result return result
} }
@@ -329,7 +329,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
result = result =
result && result &&
res.table1[index].COL4 === res.table1[index].COL4 ===
(multipleRowsWithNulls.table1[index].col4 || '') (multipleRowsWithNulls.table1[index].col4 || ' ')
}) })
return result return result
} }
@@ -358,7 +358,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
result = result =
result && result &&
res.table1[index].COL4 === res.table1[index].COL4 ===
(multipleColumnsWithNulls.table1[index].col4 || '') (multipleColumnsWithNulls.table1[index].col4 || ' ')
}) })
return result return result
} }

View File

@@ -37,8 +37,6 @@ const moreSpecialCharData: any = {
] ]
} }
const stringData: any = { table1: [{ col1: 'first col value' }] }
const getWideData = () => { const getWideData = () => {
const cols: any = {} const cols: any = {}
for (let i = 1; i <= 10000; i++) { for (let i = 1; i <= 10000; i++) {
@@ -271,34 +269,6 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
) )
} }
}, },
{
title: 'Request with extra attributes on JES approach',
description:
'Should complete successful request with extra attributes present in response',
test: async () => {
if (adapter.getSasjsConfig().serverType !== 'SASVIYA')
return Promise.resolve('skip')
const config = {
useComputeApi: false
}
return await adapter.request(
'common/sendArr',
stringData,
config,
undefined,
undefined,
['file', 'data']
)
},
assertion: (response: any) => {
if (response === 'skip') return true
const responseKeys: any = Object.keys(response)
return responseKeys.includes('file') && responseKeys.includes('data')
}
},
{ {
title: 'Special missing values', title: 'Special missing values',
description: 'Should support special missing values', description: 'Should support special missing values',

View File

@@ -592,6 +592,15 @@ export default class SASjs {
'A username and password are required when using the default login mechanism.' 'A username and password are required when using the default login mechanism.'
) )
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (!clientId)
throw new Error(
'A username, password and clientId are required when using the default login mechanism with server type SASJS.'
)
return this.authManager!.logInSasjs(username, password, clientId)
}
return this.authManager!.logIn(username, password) return this.authManager!.logIn(username, password)
} }

View File

@@ -3,6 +3,7 @@ import { ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs' import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs' import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
import { parseWeboutResponse } from './utils' import { parseWeboutResponse } from './utils'
import { getTokens } from './auth/getTokens' import { getTokens } from './auth/getTokens'
@@ -113,6 +114,20 @@ export class SASjsApiClient {
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> { public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
return refreshTokensForSasjs(this.requestClient, refreshToken) return refreshTokensForSasjs(this.requestClient, refreshToken)
} }
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
public async getAuthCode(
username: string,
password: string,
clientId: string
) {
return getAuthCodeForSasjs(this.requestClient, username, password, clientId)
}
} }
// todo move to sasjs/utils // todo move to sasjs/utils

View File

@@ -2,6 +2,8 @@ import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { LoginOptions, LoginResult } from '../types/Login' import { LoginOptions, LoginResult } from '../types/Login'
import { serialize } from '../utils' import { serialize } from '../utils'
import { getAccessTokenForSasjs } from './getAccessTokenForSasjs'
import { getAuthCodeForSasjs } from './getAuthCodeForSasjs'
import { openWebPage } from './openWebPage' import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login' import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin' import { verifySasViyaLogin } from './verifySasViyaLogin'
@@ -81,6 +83,39 @@ export class AuthManager {
return { isLoggedIn: false, userName: '' } return { isLoggedIn: false, userName: '' }
} }
/**
* Logs into the SAS server with the supplied credentials.
* @param userName - a string representing the username.
* @param password - a string representing the password.
* @param clientId - a string representing the client ID.
* @returns - a boolean `isLoggedin` and a string `username`
*/
public async logInSasjs(
username: string,
password: string,
clientId: string
): Promise<LoginResult> {
const isLoggedIn = await this.sendLoginRequestSasjs(
username,
password,
clientId
)
.then((res) => {
this.userName = username
this.requestClient.saveLocalStorageToken(
res.access_token,
res.refresh_token
)
return true
})
.catch(() => false)
return {
isLoggedIn,
userName: this.userName
}
}
/** /**
* Logs into the SAS server with the supplied credentials. * Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username. * @param username - a string representing the username.
@@ -117,7 +152,7 @@ export class AuthManager {
let loginResponse = await this.sendLoginRequest(loginForm, loginParams) let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let isLoggedIn = isLogInSuccess(this.serverType, loginResponse) let isLoggedIn = isLogInSuccess(loginResponse)
if (!isLoggedIn) { if (!isLoggedIn) {
if (isCredentialsVerifyError(loginResponse)) { if (isCredentialsVerifyError(loginResponse)) {
@@ -161,17 +196,6 @@ export class AuthManager {
loginForm: { [key: string]: any }, loginForm: { [key: string]: any },
loginParams: { [key: string]: any } loginParams: { [key: string]: any }
) { ) {
if (this.serverType === ServerType.Sasjs) {
const { username, password } = loginParams
const { result: loginResponse } = await this.requestClient.post<string>(
this.loginUrl,
{ username, password },
undefined
)
return loginResponse
}
for (const key in loginForm) { for (const key in loginForm) {
loginParams[key] = loginForm[key] loginParams[key] = loginForm[key]
} }
@@ -191,6 +215,19 @@ export class AuthManager {
return loginResponse return loginResponse
} }
private async sendLoginRequestSasjs(
username: string,
password: string,
clientId: string
) {
const authCode = await getAuthCodeForSasjs(
this.requestClient,
username,
password,
clientId
)
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
}
/** /**
* Checks whether a session is active, or login is required. * Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing three values * @returns - a promise which resolves with an object containing three values
@@ -211,7 +248,8 @@ export class AuthManager {
//Residue can happen in case of session expiration //Residue can happen in case of session expiration
await this.logOut() await this.logOut()
loginForm = await this.getNewLoginForm() if (this.serverType !== ServerType.Sasjs)
loginForm = await this.getNewLoginForm()
} }
return Promise.resolve({ return Promise.resolve({
@@ -222,12 +260,6 @@ export class AuthManager {
} }
private async getNewLoginForm() { private async getNewLoginForm() {
if (this.serverType === ServerType.Sasjs) {
// server will be sending CSRF cookie,
// http client will use it automatically
return this.requestClient.get('/', undefined)
}
const { result: formResponse } = await this.requestClient.get<string>( const { result: formResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''), this.loginUrl.replace('.do', ''),
undefined, undefined,
@@ -257,12 +289,6 @@ export class AuthManager {
const isLoggedIn = loginResponse !== 'authErr' const isLoggedIn = loginResponse !== 'authErr'
const userName = isLoggedIn ? this.extractUserName(loginResponse) : '' const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
if (!isLoggedIn) {
//We will logout to make sure cookies are removed and login form is presented
//Residue can happen in case of session expiration
await this.logOut()
}
return { isLoggedIn, userName } return { isLoggedIn, userName }
} }
@@ -358,8 +384,5 @@ const isCredentialsVerifyError = (response: string): boolean =>
response response
) )
const isLogInSuccess = (serverType: ServerType, response: any): boolean => { const isLogInSuccess = (response: string): boolean =>
if (serverType === ServerType.Sasjs) return response?.loggedin /You have signed in/gm.test(response)
return /You have signed in/gm.test(response)
}

View File

@@ -0,0 +1,31 @@
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient'
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param requestClient - the pre-configured HTTP request client
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
export const getAuthCodeForSasjs = async (
requestClient: RequestClient,
username: string,
password: string,
clientId: string
) => {
const url = '/SASjsApi/auth/authorize'
const data = { username, password, clientId }
const { code: authCode } = await requestClient
.post<{ code: string }>(url, data, undefined)
.then((res) => res.result)
.catch((err) => {
throw prefixMessage(
err,
'Error while authenticating with provided username, password and clientId. '
)
})
return authCode
}

View File

@@ -92,7 +92,7 @@ export class WebJobExecutor extends BaseJobExecutor {
if (jobUri.length > 0) { if (jobUri.length > 0) {
apiUrl += '&_job=' + jobUri apiUrl += '&_job=' + jobUri
/** /**
* Using both _job and _program parameters will cause a conflict in the JES web app, as it's not clear whether or not the server should make the extra fetch for the job uri. * Using both _job and _program parameters will cause a conflict in the JES web app, as its not clear whether or not the server should make the extra fetch for the job uri.
* To handle this, we add the extra underscore and recreate the _program variable in the SAS side of the SASjs adapter so it remains available for backend developers. * To handle this, we add the extra underscore and recreate the _program variable in the SAS side of the SASjs adapter so it remains available for backend developers.
*/ */
apiUrl = apiUrl.replace('_program=', '__program=') apiUrl = apiUrl.replace('_program=', '__program=')
@@ -169,17 +169,13 @@ export class WebJobExecutor extends BaseJobExecutor {
? res.result.log.map((logLine: any) => logLine.line).join('\n') ? res.result.log.map((logLine: any) => logLine.line).join('\n')
: res.result.log : res.result.log
const resObj = res const resObj =
this.serverType === ServerType.Sasjs
if (this.serverType === ServerType.Sasjs) { ? {
if (res.result._webout.length < 1) result: res.result._webout,
throw new JobExecutionError( log: parsedSasjsServerLog
0, }
'Job execution failed', : res
parsedSasjsServerLog
)
}
this.requestClient!.appendRequest(resObj, sasJob, config.debug) this.requestClient!.appendRequest(resObj, sasJob, config.debug)
let jsonResponse = res.result let jsonResponse = res.result

View File

@@ -19,7 +19,6 @@ import {
parseSourceCode, parseSourceCode,
createAxiosInstance createAxiosInstance
} from '../utils' } from '../utils'
import { InvalidCsrfError } from '../types/errors/InvalidCsrfError'
export interface HttpClient { export interface HttpClient {
get<T>( get<T>(
@@ -499,24 +498,6 @@ export class RequestClient implements HttpClient {
throw e throw e
} }
if (e instanceof InvalidCsrfError) {
// Fetching root will inject CSRF token in cookie
await this.httpClient
.get('/', {
withCredentials: true
})
.catch((err) => {
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
})
return await callback().catch((err: any) => {
throw prefixMessage(
err,
'Error while executing callback in handleError. '
)
})
}
if (response?.status === 403 || response?.status === 449) { if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response) this.parseAndSetCsrfToken(response)
@@ -603,17 +584,9 @@ export class RequestClient implements HttpClient {
export const throwIfError = (response: AxiosResponse) => { export const throwIfError = (response: AxiosResponse) => {
switch (response.status) { switch (response.status) {
case 400: case 400:
if ( if (typeof response.data === 'object') {
typeof response.data === 'object' &&
response.data.error === 'invalid_grant'
) {
// In SASVIYA when trying to get access token, if auth code is wrong status code will be 400 so in such case we return login required error.
throw new LoginRequiredError(response.data) throw new LoginRequiredError(response.data)
} }
if (response.data.toLowerCase() === 'invalid csrf token!') {
throw new InvalidCsrfError()
}
break break
case 401: case 401:
if (typeof response.data === 'object') { if (typeof response.data === 'object') {

View File

@@ -1,9 +0,0 @@
export class InvalidCsrfError extends Error {
constructor() {
const message = 'Invalid CSRF token!'
super(`Auth error: ${message}`)
this.name = 'InvalidCsrfError'
Object.setPrototypeOf(this, InvalidCsrfError.prototype)
}
}

View File

@@ -1,4 +1,4 @@
import { isSpecialMissing } from '@sasjs/utils/input/validators' import { isSpecialMissing } from '@sasjs/utils'
/** /**
* Converts the given JSON object array to a CSV string. * Converts the given JSON object array to a CSV string.