mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-04 03:00:05 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e23b5db9d | ||
| 78f117812e | |||
|
|
55af8c3f50 | ||
| 1185c2f1bf | |||
|
|
2842636c4a | ||
| 8c7f614509 | |||
| 943f60ea11 | |||
| 3de343f135 | |||
| e11c97ec5d | |||
| 49fba07824 | |||
| b1c0e26c23 | |||
|
|
3ec73750b7 | ||
| e3c4cb6b90 | |||
| d35f1617b8 | |||
| 302752d79e | |||
| 4e1e3e8e77 | |||
| 954d3ff633 | |||
| fce0c7e522 | |||
| d0fbc7b8c7 | |||
| 6171199a7e | |||
| 4fb0b96f11 | |||
| 008a9b4ca5 | |||
| b3b2c1414c | |||
| 18be9e8806 | |||
| 7bdd826418 | |||
| 3713a226a4 | |||
| 77306fedee | |||
| be3ce56b85 | |||
| 851b8fce2a | |||
|
|
16dd175053 | ||
| 27698b3e8a | |||
| 0faa50685d | |||
|
|
0f20048fb4 | ||
| 249837dacf | |||
| a115c12f55 | |||
| 61c4d21467 | |||
| 3e9f38529f | |||
|
|
06f79307b9 | ||
|
|
5122d2a9c9 | ||
|
|
dc3eb3f0db | ||
|
|
b940bc7cc3 | ||
|
|
82fc55ac1c | ||
| fc1a22c8c5 | |||
| 57b9f86077 | |||
|
|
68f7b2eac2 | ||
|
|
2676873bb0 | ||
| add2f0a860 | |||
| 2072136577 | |||
| afae632fc6 | |||
|
|
317587a3c8 | ||
|
|
ffd6bc5a5c | ||
|
|
c2e64d9ba6 | ||
|
|
a90f699abd | ||
|
|
2cca192f88 | ||
|
|
053b07769a |
8
.github/vpn/config.ovpn
vendored
8
.github/vpn/config.ovpn
vendored
@@ -3,10 +3,12 @@ client
|
|||||||
tls-client
|
tls-client
|
||||||
dev tun
|
dev tun
|
||||||
# this will connect with whatever proto DNS tells us (https://community.openvpn.net/openvpn/ticket/934)
|
# this will connect with whatever proto DNS tells us (https://community.openvpn.net/openvpn/ticket/934)
|
||||||
proto tcp
|
proto udp
|
||||||
remote vpn.4gl.io 7494
|
remote vpn.4gl.io 7194
|
||||||
resolv-retry infinite
|
resolv-retry infinite
|
||||||
cipher AES-256-CBC
|
# this will fallback from udp6 to udp4 as well
|
||||||
|
connect-timeout 5
|
||||||
|
data-ciphers AES-256-CBC:AES-256-GCM
|
||||||
auth SHA256
|
auth SHA256
|
||||||
script-security 2
|
script-security 2
|
||||||
keepalive 10 120
|
keepalive 10 120
|
||||||
|
|||||||
58
.github/workflows/build-unit-tests.yml
vendored
Normal file
58
.github/workflows/build-unit-tests.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: SASjs Build and Unit Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [lts/hydrogen]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
# 2. Restore npm cache manually
|
||||||
|
- name: Restore npm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: npm-cache
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
|
- name: Check npm audit
|
||||||
|
run: npm audit --production --audit-level=low
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Rimraf
|
||||||
|
run: npm i rimraf
|
||||||
|
|
||||||
|
- name: Check code style
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Build Package
|
||||||
|
run: npm run package:lib
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
# 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
|
||||||
|
uses: artiomtr/jest-coverage-report-action@v2.0-rc.2
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
11
.github/workflows/generateDocs.yml
vendored
11
.github/workflows/generateDocs.yml
vendored
@@ -21,7 +21,16 @@ jobs:
|
|||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: npm
|
|
||||||
|
# 2. Restore npm cache manually
|
||||||
|
- name: Restore npm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: npm-cache
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
11
.github/workflows/npmpublish.yml
vendored
11
.github/workflows/npmpublish.yml
vendored
@@ -22,7 +22,16 @@ jobs:
|
|||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: npm
|
|
||||||
|
# 2. Restore npm cache manually
|
||||||
|
- name: Restore npm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: npm-cache
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
name: SASjs Build
|
name: SASjs Build and Server Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -20,20 +20,22 @@ jobs:
|
|||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: npm
|
|
||||||
|
|
||||||
# FIXME: uncomment 'Check npm audit' step after axios version bump
|
# 2. Restore npm cache manually
|
||||||
# - name: Check npm audit
|
- name: Restore npm cache
|
||||||
# run: npm audit --production --audit-level=low
|
uses: actions/cache@v3
|
||||||
|
id: npm-cache
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Check code style
|
- name: Install Rimraf
|
||||||
run: npm run lint
|
run: npm i rimraf
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: npm test
|
|
||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: npm run package:lib
|
run: npm run package:lib
|
||||||
@@ -53,6 +55,10 @@ jobs:
|
|||||||
USER_KEY: ${{ secrets.USER_KEY }}
|
USER_KEY: ${{ secrets.USER_KEY }}
|
||||||
TLS_KEY: ${{ secrets.TLS_KEY }}
|
TLS_KEY: ${{ secrets.TLS_KEY }}
|
||||||
|
|
||||||
|
- name: Chmod VPN files
|
||||||
|
run: |
|
||||||
|
chmod 600 .github/vpn/ca.crt .github/vpn/user.crt .github/vpn/user.key .github/vpn/tls.key
|
||||||
|
|
||||||
- name: Install Open VPN
|
- name: Install Open VPN
|
||||||
run: |
|
run: |
|
||||||
sudo apt install apt-transport-https
|
sudo apt install apt-transport-https
|
||||||
@@ -68,6 +74,9 @@ jobs:
|
|||||||
- name: install pm2
|
- name: install pm2
|
||||||
run: npm i -g pm2
|
run: npm i -g pm2
|
||||||
|
|
||||||
|
- name: Fetch SASJS server
|
||||||
|
run: curl ${{ secrets.SASJS_SERVER_URL }}/SASjsApi/info
|
||||||
|
|
||||||
- name: Deploy sasjs-tests
|
- name: Deploy sasjs-tests
|
||||||
run: |
|
run: |
|
||||||
npm install -g replace-in-files-cli
|
npm install -g replace-in-files-cli
|
||||||
@@ -76,8 +85,10 @@ jobs:
|
|||||||
npm i
|
npm i
|
||||||
replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json
|
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='"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
|
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
|
||||||
|
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
|
||||||
|
cat ./public/config.json
|
||||||
|
|
||||||
npm run update:adapter
|
npm run update:adapter
|
||||||
pm2 start --name sasjs-test npm -- start
|
pm2 start --name sasjs-test npm -- start
|
||||||
|
|
||||||
@@ -90,10 +101,7 @@ jobs:
|
|||||||
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
|
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
|
||||||
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./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
|
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
|
||||||
sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
|
cat ./cypress.json
|
||||||
|
echo "SASJS_USERNAME=${{ secrets.SASJS_USERNAME }}"
|
||||||
|
|
||||||
# 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
|
sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
|
||||||
- name: Generate coverage report
|
|
||||||
uses: artiomtr/jest-coverage-report-action@v2.0-rc.2
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": ["SASVIYA"]
|
||||||
|
}
|
||||||
@@ -4,93 +4,56 @@ const password = Cypress.env('password')
|
|||||||
const testingFinishTimeout = Cypress.env('testingFinishTimeout')
|
const testingFinishTimeout = Cypress.env('testingFinishTimeout')
|
||||||
|
|
||||||
context('sasjs-tests', function () {
|
context('sasjs-tests', function () {
|
||||||
this.beforeAll(() => {
|
before(() => {
|
||||||
cy.visit(sasjsTestsUrl)
|
cy.visit(sasjsTestsUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.reload()
|
cy.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have all tests successfull', (done) => {
|
function loginIfNeeded() {
|
||||||
cy.get('body').then(($body) => {
|
cy.get('body').then(($body) => {
|
||||||
cy.wait(1000).then(() => {
|
if ($body.find('input[placeholder="User Name"]').length > 0) {
|
||||||
const startButton = $body.find(
|
cy.get('input[placeholder="User Name"]')
|
||||||
'.ui.massive.icon.primary.left.labeled.button'
|
.should('be.visible')
|
||||||
)[0]
|
.type(username)
|
||||||
|
cy.get('input[placeholder="Password"]')
|
||||||
if (
|
.should('be.visible')
|
||||||
!startButton ||
|
.type(password)
|
||||||
(startButton && !Cypress.dom.isVisible(startButton))
|
cy.get('.submit-button').should('be.visible').click()
|
||||||
) {
|
cy.get('input[placeholder="User Name"]').should('not.exist') // Wait for login to finish
|
||||||
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 successful', () => {
|
||||||
|
loginIfNeeded()
|
||||||
|
|
||||||
|
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||||
|
.should('be.visible')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.ui.massive.loading.primary.button', {
|
||||||
|
timeout: testingFinishTimeout
|
||||||
|
}).should('not.exist')
|
||||||
|
|
||||||
|
cy.get('span.icon.failed').should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have all tests successfull with debug on', (done) => {
|
it('Should have all tests successful with debug on', () => {
|
||||||
cy.get('body').then(($body) => {
|
loginIfNeeded()
|
||||||
cy.wait(1000).then(() => {
|
|
||||||
const startButton = $body.find(
|
|
||||||
'.ui.massive.icon.primary.left.labeled.button'
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
if (
|
cy.get('.ui.fitted.toggle.checkbox label').should('be.visible').click()
|
||||||
!startButton ||
|
|
||||||
(startButton && !Cypress.dom.isVisible(startButton))
|
|
||||||
) {
|
|
||||||
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')
|
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||||
.click()
|
.should('be.visible')
|
||||||
.then(() => {
|
.click()
|
||||||
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
|
||||||
.should('not.exist')
|
cy.get('.ui.massive.loading.primary.button', {
|
||||||
.then(() => {
|
timeout: testingFinishTimeout
|
||||||
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
}).should('not.exist')
|
||||||
.click()
|
|
||||||
.then(() => {
|
cy.get('span.icon.failed').should('not.exist')
|
||||||
cy.get('.ui.massive.loading.primary.button', {
|
|
||||||
timeout: testingFinishTimeout
|
|
||||||
})
|
|
||||||
.should('not.exist')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('span.icon.failed')
|
|
||||||
.should('not.exist')
|
|
||||||
.then(() => {
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
BIN
cypress/videos/sasjs.tests.ts.mp4
Normal file
BIN
cypress/videos/sasjs.tests.ts.mp4
Normal file
Binary file not shown.
@@ -142,6 +142,8 @@ module.exports = {
|
|||||||
// Options that will be passed to the testEnvironment
|
// Options that will be passed to the testEnvironment
|
||||||
// testEnvironmentOptions: {},
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
testEnvironment: 'node',
|
||||||
|
|
||||||
// Adds a location field to test results
|
// Adds a location field to test results
|
||||||
// testLocationInResults: false,
|
// testLocationInResults: false,
|
||||||
|
|
||||||
|
|||||||
23290
package-lock.json
generated
23290
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -6,7 +6,7 @@
|
|||||||
"nodeVersionMessage": "echo \u001b[33m make sure you are running node lts version \u001b[0m",
|
"nodeVersionMessage": "echo \u001b[33m make sure you are running node lts version \u001b[0m",
|
||||||
"preinstall": "npm run nodeVersionMessage",
|
"preinstall": "npm run nodeVersionMessage",
|
||||||
"prebuild": "npm run nodeVersionMessage",
|
"prebuild": "npm run nodeVersionMessage",
|
||||||
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
|
"build": "npx rimraf build && npx rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && npx rimraf build/src && npx rimraf node",
|
||||||
"package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
"package:lib": "npm run build && copyfiles ./package.json 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 --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}\"",
|
||||||
@@ -45,41 +45,43 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/webpack-preprocessor": "5.9.1",
|
"@cypress/webpack-preprocessor": "5.9.1",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "4.17.13",
|
"@types/express": "4.17.13",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "29.5.14",
|
||||||
"@types/mime": "2.0.3",
|
"@types/mime": "2.0.3",
|
||||||
"@types/pem": "1.9.6",
|
"@types/pem": "1.9.6",
|
||||||
"@types/tough-cookie": "4.0.2",
|
"@types/tough-cookie": "4.0.2",
|
||||||
"copyfiles": "2.4.1",
|
"copyfiles": "2.4.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"cp": "0.2.0",
|
"cp": "0.2.0",
|
||||||
"cypress": "7.7.0",
|
"cypress": "7.7.0",
|
||||||
"dotenv": "16.0.0",
|
"dotenv": "16.0.0",
|
||||||
"express": "4.17.3",
|
"express": "4.17.3",
|
||||||
"jest": "27.4.7",
|
"jest": "29.7.0",
|
||||||
"jest-extended": "2.0.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"jest-extended": "4.0.2",
|
||||||
"node-polyfill-webpack-plugin": "1.1.4",
|
"node-polyfill-webpack-plugin": "1.1.4",
|
||||||
"path": "0.12.7",
|
"path": "0.12.7",
|
||||||
"pem": "1.14.5",
|
"pem": "1.14.5",
|
||||||
"prettier": "2.8.7",
|
"prettier": "2.8.7",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
"rimraf": "3.0.2",
|
|
||||||
"semantic-release": "19.0.3",
|
"semantic-release": "19.0.3",
|
||||||
"terser-webpack-plugin": "5.3.6",
|
"terser-webpack-plugin": "5.3.6",
|
||||||
"ts-jest": "27.1.3",
|
"ts-jest": "29.2.6",
|
||||||
"ts-loader": "9.4.0",
|
"ts-loader": "9.4.0",
|
||||||
"tslint": "6.1.3",
|
"tslint": "6.1.3",
|
||||||
"tslint-config-prettier": "1.18.0",
|
"tslint-config-prettier": "1.18.0",
|
||||||
"typedoc": "0.23.24",
|
"typedoc": "0.23.24",
|
||||||
"typedoc-plugin-rename-defaults": "0.6.4",
|
"typedoc-plugin-rename-defaults": "0.6.4",
|
||||||
"typescript": "4.8.3",
|
"typescript": "4.9.5",
|
||||||
"webpack": "5.76.2",
|
"webpack": "5.76.2",
|
||||||
"webpack-cli": "4.9.2"
|
"webpack-cli": "4.9.2"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "2.52.0",
|
"@sasjs/utils": "3.5.2",
|
||||||
"axios": "0.27.2",
|
"axios": "1.8.2",
|
||||||
"axios-cookiejar-support": "1.0.1",
|
"axios-cookiejar-support": "5.0.5",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"https": "1.0.0",
|
"https": "1.0.0",
|
||||||
"tough-cookie": "4.1.3"
|
"tough-cookie": "4.1.3"
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
SKIP_PREFLIGHT_CHECK=true
|
SKIP_PREFLIGHT_CHECK=true
|
||||||
|
# Removes index.html inline scripts
|
||||||
|
INLINE_RUNTIME_CHUNK=false
|
||||||
|
|||||||
15
sasjs-tests/craco.config.js
Normal file
15
sasjs-tests/craco.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// craco.config.js
|
||||||
|
// We use craco instead of react-scripts so we can override webpack config, to include source maps
|
||||||
|
// so we can debug @sasjs/adapter easier when tests fail
|
||||||
|
module.exports = {
|
||||||
|
webpack: {
|
||||||
|
configure: (webpackConfig, { env }) => {
|
||||||
|
// Disable optimizations in both development and production
|
||||||
|
webpackConfig.optimization.minimize = false;
|
||||||
|
webpackConfig.optimization.minimizer = [];
|
||||||
|
webpackConfig.optimization.concatenateModules = false;
|
||||||
|
webpackConfig.optimization.splitChunks = { cacheGroups: { default: false } };
|
||||||
|
return webpackConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
37878
sasjs-tests/package-lock.json
generated
37878
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
|||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||||
"@sasjs/test-framework": "1.5.7",
|
"@sasjs/test-framework": "1.5.7",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^14.14.41",
|
"@types/node": "^14.14.41",
|
||||||
@@ -13,12 +14,12 @@
|
|||||||
"react": "^16.0.1",
|
"react": "^16.0.1",
|
||||||
"react-dom": "^16.0.1",
|
"react-dom": "^16.0.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "4.0.3",
|
||||||
"typescript": "^4.1.3"
|
"typescript": "^4.1.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "NODE_OPTIONS=--openssl-legacy-provider react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "NODE_OPTIONS=--openssl-legacy-provider craco build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
|
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"node-sass": "9.0.0"
|
"@craco/craco": "6.4.3",
|
||||||
|
"node-sass": "9.0.0",
|
||||||
|
"source-map-loader": "0.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
|
|||||||
echo "Cypress sasjs testing passed!"
|
echo "Cypress sasjs testing passed!"
|
||||||
else
|
else
|
||||||
echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}'
|
echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}'
|
||||||
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io:4gl.io/send/m.room.message?access_token=$1
|
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io/send/m.room.message?access_token=$1
|
||||||
echo "Cypress sasjs testing failed!"
|
echo "Cypress sasjs testing failed!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -24,6 +24,26 @@
|
|||||||
"streamServiceName": "adapter-tests",
|
"streamServiceName": "adapter-tests",
|
||||||
"assetPaths": []
|
"assetPaths": []
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "viya",
|
||||||
|
"serverUrl": "",
|
||||||
|
"serverType": "SASVIYA",
|
||||||
|
"httpsAgentOptions": {
|
||||||
|
"allowInsecureRequests": false
|
||||||
|
},
|
||||||
|
"appLoc": "/Public/app/adapter-tests",
|
||||||
|
"deployConfig": {
|
||||||
|
"deployServicePack": true,
|
||||||
|
"deployScripts": []
|
||||||
|
},
|
||||||
|
"streamConfig": {
|
||||||
|
"streamWeb": true,
|
||||||
|
"streamWebFolder": "webv",
|
||||||
|
"webSourcePath": "build",
|
||||||
|
"streamServiceName": "adapter-tests",
|
||||||
|
"assetPaths": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const App = (): ReactElement<{}> => {
|
|||||||
basicTests(adapter, config.userName, config.password),
|
basicTests(adapter, config.userName, config.password),
|
||||||
sendArrTests(adapter, appLoc),
|
sendArrTests(adapter, appLoc),
|
||||||
sendObjTests(adapter),
|
sendObjTests(adapter),
|
||||||
specialCaseTests(adapter),
|
// specialCaseTests(adapter),
|
||||||
sasjsRequestTests(adapter),
|
sasjsRequestTests(adapter),
|
||||||
fileUploadTests(adapter)
|
fileUploadTests(adapter)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -87,6 +87,20 @@ export const basicTests = (
|
|||||||
return response.table1[0][0] === stringData.table1[0].col1
|
return response.table1[0][0] === stringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Web request',
|
||||||
|
description: 'Should run the request with old web approach',
|
||||||
|
test: async () => {
|
||||||
|
const config: Partial<SASjsConfig> = {
|
||||||
|
useComputeApi: false
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter.request('common/sendArr', stringData, config)
|
||||||
|
},
|
||||||
|
assertion: (response: any) => {
|
||||||
|
return response.table1[0][0] === stringData.table1[0].col1
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Request with debug on',
|
title: 'Request with debug on',
|
||||||
description:
|
description:
|
||||||
@@ -159,20 +173,6 @@ export const basicTests = (
|
|||||||
sasjsConfig.debug === false
|
sasjsConfig.debug === false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Web request',
|
|
||||||
description: 'Should run the request with old web approach',
|
|
||||||
test: async () => {
|
|
||||||
const config: Partial<SASjsConfig> = {
|
|
||||||
useComputeApi: false
|
|
||||||
}
|
|
||||||
|
|
||||||
return await adapter.request('common/sendArr', stringData, config)
|
|
||||||
},
|
|
||||||
assertion: (response: any) => {
|
|
||||||
return response.table1[0][0] === stringData.table1[0].col1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,30 +20,30 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
|||||||
return requests[0].SASWORK === null
|
return requests[0].SASWORK === null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Make error and capture log',
|
|
||||||
description:
|
|
||||||
'Should make an error and capture log, in the same time it is testing if debug override is working',
|
|
||||||
test: async () => {
|
|
||||||
return adapter
|
|
||||||
.request('common/makeErr', data, { debug: true })
|
|
||||||
.catch(() => {
|
|
||||||
const sasRequests = adapter.getSasRequests()
|
|
||||||
const makeErrRequest: any =
|
|
||||||
sasRequests.find((req) => req.serviceLink.includes('makeErr')) ||
|
|
||||||
null
|
|
||||||
|
|
||||||
if (!makeErrRequest) return false
|
|
||||||
|
|
||||||
return !!(
|
|
||||||
makeErrRequest.logFile && makeErrRequest.logFile.length > 0
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
assertion: (response) => {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// title: 'Make error and capture log',
|
||||||
|
// description:
|
||||||
|
// 'Should make an error and capture log, in the same time it is testing if debug override is working',
|
||||||
|
// test: async () => {
|
||||||
|
// return adapter
|
||||||
|
// .request('common/makeErr', data, { debug: true })
|
||||||
|
// .catch(() => {
|
||||||
|
// const sasRequests = adapter.getSasRequests()
|
||||||
|
// const makeErrRequest: any =
|
||||||
|
// sasRequests.find((req) => req.serviceLink.includes('makeErr')) ||
|
||||||
|
// null
|
||||||
|
|
||||||
|
// if (!makeErrRequest) return false
|
||||||
|
|
||||||
|
// return !!(
|
||||||
|
// makeErrRequest.logFile && makeErrRequest.logFile.length > 0
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// assertion: (response) => {
|
||||||
|
// return response
|
||||||
|
// }
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -134,6 +134,20 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
return adapter.request('common/sendArr', moreSpecialCharData)
|
return adapter.request('common/sendArr', moreSpecialCharData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
|
// If sas session is `latin9` or `wlatin1` we can't process the special characters,
|
||||||
|
// But it can happen that response is broken JSON, so we first need to check if
|
||||||
|
// it's object and then check accordingly
|
||||||
|
|
||||||
|
if (typeof res === 'object') {
|
||||||
|
// Valid JSON response
|
||||||
|
if (res.SYSENCODING === 'latin9' || res.SYSENCODING === 'wlatin1')
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// Since we got string response (broken JSON), we need to check with regex
|
||||||
|
const regex = /"SYSENCODING"\s*:\s*"(?:wlatin1|latin9)"/
|
||||||
|
if (regex.test(res)) return true
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
res.table1[0][0] === moreSpecialCharData.table1[0].speech0 &&
|
res.table1[0][0] === moreSpecialCharData.table1[0].speech0 &&
|
||||||
res.table1[0][1] === moreSpecialCharData.table1[0].pct &&
|
res.table1[0][1] === moreSpecialCharData.table1[0].pct &&
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
|
"sourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { generateTimestamp } from '@sasjs/utils/time'
|
import { generateTimestamp } from '@sasjs/utils/time'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { Sas9RequestClient } from './request/Sas9RequestClient'
|
import { Sas9RequestClient } from './request/Sas9RequestClient'
|
||||||
import { isUrl } from './utils'
|
import { isUrl } from './utils'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isRelativePath, isUri, isUrl } from './utils'
|
import { isRelativePath, isUri, isUrl } from './utils'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import {
|
import {
|
||||||
Job,
|
Job,
|
||||||
Session,
|
Session,
|
||||||
@@ -28,6 +28,7 @@ import { uploadTables } from './api/viya/uploadTables'
|
|||||||
import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
|
import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
|
||||||
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
||||||
import { refreshTokensForViya } from './auth/refreshTokensForViya'
|
import { refreshTokensForViya } from './auth/refreshTokensForViya'
|
||||||
|
import { FileResource } from './types/FileResource'
|
||||||
|
|
||||||
interface JobExecutionResult {
|
interface JobExecutionResult {
|
||||||
result?: { result: object }
|
result?: { result: object }
|
||||||
@@ -311,6 +312,84 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the file content for a file in the specified folder.
|
||||||
|
*
|
||||||
|
* @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
|
||||||
|
* @param fileName - the name of the file in the `folderPath`
|
||||||
|
* @param accessToken - an access token for authorizing the request
|
||||||
|
*/
|
||||||
|
public async getFileContent(
|
||||||
|
folderPath: string,
|
||||||
|
fileName: string,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
const fileUri = await this.getFileUri(
|
||||||
|
folderPath,
|
||||||
|
fileName,
|
||||||
|
accessToken
|
||||||
|
).catch((err) => {
|
||||||
|
throw prefixMessage(
|
||||||
|
err,
|
||||||
|
`Error while getting file URI for: ${fileName} in folder: ${folderPath}. `
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.requestClient
|
||||||
|
.get<string>(`${this.serverUrl}${fileUri}/content`, accessToken)
|
||||||
|
.then((res) => res.result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the file content for a file in the specified folder.
|
||||||
|
*
|
||||||
|
* @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
|
||||||
|
* @param fileName - the name of the file in the `folderPath`
|
||||||
|
* @param content - the new content to be written to the file
|
||||||
|
* @param accessToken - an access token for authorizing the request
|
||||||
|
*/
|
||||||
|
public async updateFileContent(
|
||||||
|
folderPath: string,
|
||||||
|
fileName: string,
|
||||||
|
content: string,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
const fileUri = await this.getFileUri(
|
||||||
|
folderPath,
|
||||||
|
fileName,
|
||||||
|
accessToken
|
||||||
|
).catch((err) => {
|
||||||
|
throw prefixMessage(
|
||||||
|
err,
|
||||||
|
`Error while getting file URI for: ${fileName} in folder: ${folderPath}. `
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch the file resource details to get the Etag and content type
|
||||||
|
const { result: originalFileResource, etag } =
|
||||||
|
await this.requestClient.get<FileResource>(
|
||||||
|
`${this.serverUrl}${fileUri}`,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!originalFileResource || !etag)
|
||||||
|
throw new Error(
|
||||||
|
`File ${fileName} does not have an ETag, or request failed.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return await this.requestClient
|
||||||
|
.put<FileResource>(
|
||||||
|
`${this.serverUrl}${fileUri}/content`,
|
||||||
|
content,
|
||||||
|
accessToken,
|
||||||
|
{
|
||||||
|
'If-Match': etag,
|
||||||
|
'Content-Type': originalFileResource.contentType
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => res.result)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a folder. Path to the folder is required.
|
* Fetches a folder. Path to the folder is required.
|
||||||
* @param folderPath - the absolute path to the folder.
|
* @param folderPath - the absolute path to the folder.
|
||||||
@@ -791,14 +870,14 @@ export class SASViyaApiClient {
|
|||||||
_webin_file_count: files.length,
|
_webin_file_count: files.length,
|
||||||
_OMITJSONLISTING: true,
|
_OMITJSONLISTING: true,
|
||||||
_OMITJSONLOG: true,
|
_OMITJSONLOG: true,
|
||||||
_OMITSESSIONRESULTS: true,
|
_omitSessionResults: false,
|
||||||
_OMITTEXTLISTING: true,
|
_OMITTEXTLISTING: true,
|
||||||
_OMITTEXTLOG: true
|
_OMITTEXTLOG: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
jobArguments['_OMITTEXTLOG'] = 'false'
|
jobArguments['_OMITTEXTLOG'] = 'false'
|
||||||
jobArguments['_OMITSESSIONRESULTS'] = 'false'
|
jobArguments['_omitSessionResults'] = 'false'
|
||||||
jobArguments['_DEBUG'] = 131
|
jobArguments['_DEBUG'] = 131
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -941,6 +1020,7 @@ export class SASViyaApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!folder) return undefined
|
if (!folder) return undefined
|
||||||
|
|
||||||
return folder
|
return folder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -952,6 +1032,30 @@ export class SASViyaApiClient {
|
|||||||
return `/folders/folders/${folderDetails.id}`
|
return `/folders/folders/${folderDetails.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getFileUri(
|
||||||
|
folderPath: string,
|
||||||
|
fileName: string,
|
||||||
|
accessToken?: string
|
||||||
|
): Promise<string> {
|
||||||
|
const folderMembers = await this.listFolder(folderPath, accessToken, 1000, {
|
||||||
|
returnDetails: true
|
||||||
|
}).catch((err) => {
|
||||||
|
throw prefixMessage(err, `Error while listing folder: ${folderPath}. `)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!folderMembers || !folderMembers.length)
|
||||||
|
throw new Error(`No members found in folder: ${folderPath}`)
|
||||||
|
|
||||||
|
const fileUri = folderMembers.find(
|
||||||
|
(member) => member.name === fileName
|
||||||
|
)?.uri
|
||||||
|
|
||||||
|
if (!fileUri)
|
||||||
|
throw new Error(`File ${fileName} not found in folder: ${folderPath}`)
|
||||||
|
|
||||||
|
return fileUri
|
||||||
|
}
|
||||||
|
|
||||||
private async getRecycleBinUri(accessToken?: string) {
|
private async getRecycleBinUri(accessToken?: string) {
|
||||||
const url = '/folders/folders/@myRecycleBin'
|
const url = '/folders/folders/@myRecycleBin'
|
||||||
|
|
||||||
@@ -999,14 +1103,19 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists children folders for given Viya folder.
|
* Lists children folders/files for given Viya folder.
|
||||||
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
|
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
|
||||||
* @param accessToken - an access token for authorizing the request.
|
* @param accessToken - an access token for authorizing the request.
|
||||||
|
* @param {Object} [options] - Additional options.
|
||||||
|
* @param {boolean} [options.returnDetails=false] - when set to true, the function will return an array of objects with member details, otherwise it will return an array of member names.
|
||||||
*/
|
*/
|
||||||
public async listFolder(
|
public async listFolder(
|
||||||
sourceFolder: string,
|
sourceFolder: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
limit: number = 20
|
limit: number = 20,
|
||||||
|
options?: {
|
||||||
|
returnDetails?: boolean
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
// checks if 'sourceFolder' is already a URI
|
// checks if 'sourceFolder' is already a URI
|
||||||
const sourceFolderUri = isUri(sourceFolder)
|
const sourceFolderUri = isUri(sourceFolder)
|
||||||
@@ -1018,11 +1127,20 @@ export class SASViyaApiClient {
|
|||||||
accessToken
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let membersToReturn = []
|
||||||
|
|
||||||
if (members && members.items) {
|
if (members && members.items) {
|
||||||
return members.items.map((item: any) => item.name)
|
// If returnDetails is true, return full member details
|
||||||
} else {
|
if (options?.returnDetails) {
|
||||||
return []
|
membersToReturn = members.items
|
||||||
|
} else {
|
||||||
|
// If returnDetails is false, return only member names
|
||||||
|
membersToReturn = members.items.map((item: any) => item.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return members without Etag
|
||||||
|
return membersToReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
58
src/SASjs.ts
58
src/SASjs.ts
@@ -411,6 +411,51 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the file content for a file in the specified folder.
|
||||||
|
*
|
||||||
|
* @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
|
||||||
|
* @param fileName - the name of the file in the `folderPath`
|
||||||
|
* @param accessToken - an access token for authorizing the request
|
||||||
|
*/
|
||||||
|
public async getFileContent(
|
||||||
|
folderPath: string,
|
||||||
|
fileName: string,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
this.isMethodSupported('getFileContent', [ServerType.SasViya])
|
||||||
|
|
||||||
|
return await this.sasViyaApiClient!.getFileContent(
|
||||||
|
folderPath,
|
||||||
|
fileName,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the file content for a file in the specified folder.
|
||||||
|
*
|
||||||
|
* @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
|
||||||
|
* @param fileName - the name of the file in the `folderPath`
|
||||||
|
* @param content - the new content to be written to the file
|
||||||
|
* @param accessToken - an access token for authorizing the request
|
||||||
|
*/
|
||||||
|
public async updateFileContent(
|
||||||
|
folderPath: string,
|
||||||
|
fileName: string,
|
||||||
|
content: string,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
this.isMethodSupported('updateFileContent', [ServerType.SasViya])
|
||||||
|
|
||||||
|
return await this.sasViyaApiClient!.updateFileContent(
|
||||||
|
folderPath,
|
||||||
|
fileName,
|
||||||
|
content,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a folder from the SAS file system.
|
* Fetches a folder from the SAS file system.
|
||||||
* @param folderPath - path of the folder to be fetched.
|
* @param folderPath - path of the folder to be fetched.
|
||||||
@@ -436,18 +481,23 @@ export default class SASjs {
|
|||||||
* Lists children folders for given Viya folder.
|
* Lists children folders for given Viya folder.
|
||||||
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
|
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
|
||||||
* @param accessToken - an access token for authorizing the request.
|
* @param accessToken - an access token for authorizing the request.
|
||||||
|
* @param returnDetails - when set to true, the function will return an array of objects with member details, otherwise it will return an array of member names.
|
||||||
*/
|
*/
|
||||||
public async listFolder(
|
public async listFolder(
|
||||||
sourceFolder: string,
|
sourceFolder: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
limit?: number
|
limit?: number,
|
||||||
|
returnDetails = false
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('listFolder', [ServerType.SasViya])
|
this.isMethodSupported('listFolder', [ServerType.SasViya])
|
||||||
|
|
||||||
return await this.sasViyaApiClient?.listFolder(
|
return await this.sasViyaApiClient?.listFolder(
|
||||||
sourceFolder,
|
sourceFolder,
|
||||||
accessToken,
|
accessToken,
|
||||||
limit
|
limit,
|
||||||
|
{
|
||||||
|
returnDetails
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,8 +1220,8 @@ export default class SASjs {
|
|||||||
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
||||||
*/
|
*/
|
||||||
public enableVerboseMode(
|
public enableVerboseMode(
|
||||||
successCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse,
|
successCallBack?: (response: AxiosResponse) => AxiosResponse,
|
||||||
errorCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse
|
errorCallBack?: (response: AxiosError) => AxiosError
|
||||||
) {
|
) {
|
||||||
this.requestClient?.enableVerboseMode(successCallBack, errorCallBack)
|
this.requestClient?.enableVerboseMode(successCallBack, errorCallBack)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
|
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { ExecutionQuery } from './types'
|
import { ExecutionQuery } from './types'
|
||||||
|
|||||||
@@ -69,5 +69,5 @@ const setupMocks = () => {
|
|||||||
.mockImplementation(() => Promise.resolve('Test Log'))
|
.mockImplementation(() => Promise.resolve('Test Log'))
|
||||||
jest
|
jest
|
||||||
.spyOn(writeStreamModule, 'writeStream')
|
.spyOn(writeStreamModule, 'writeStream')
|
||||||
.mockImplementation(() => Promise.resolve())
|
.mockImplementation(() => Promise.resolve(true))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,22 @@ import {
|
|||||||
describe('writeStream', () => {
|
describe('writeStream', () => {
|
||||||
const filename = 'test.txt'
|
const filename = 'test.txt'
|
||||||
const content = 'test'
|
const content = 'test'
|
||||||
|
|
||||||
let stream: WriteStream
|
let stream: WriteStream
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
stream = await createWriteStream(filename)
|
stream = await createWriteStream(filename)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await deleteFile(filename).catch(() => {}) // Ignore errors if the file doesn't exist
|
||||||
|
stream = await createWriteStream(filename)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteFile(filename).catch(() => {}) // Ensure cleanup after test
|
||||||
|
})
|
||||||
|
|
||||||
it('should resolve when the stream is written successfully', async () => {
|
it('should resolve when the stream is written successfully', async () => {
|
||||||
await expect(writeStream(stream, content)).toResolve()
|
await expect(writeStream(stream, content)).toResolve()
|
||||||
await expect(fileExists(filename)).resolves.toEqual(true)
|
await expect(fileExists(filename)).resolves.toEqual(true)
|
||||||
@@ -25,11 +35,30 @@ describe('writeStream', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should reject when the write errors out', async () => {
|
it('should reject when the write errors out', async () => {
|
||||||
|
// Mock implementation of the write method
|
||||||
jest
|
jest
|
||||||
.spyOn(stream, 'write')
|
.spyOn(stream, 'write')
|
||||||
.mockImplementation((_, callback) => callback(new Error('Test Error')))
|
.mockImplementation(
|
||||||
|
(
|
||||||
|
chunk: any,
|
||||||
|
encodingOrCb?:
|
||||||
|
| BufferEncoding
|
||||||
|
| ((error: Error | null | undefined) => void),
|
||||||
|
cb?: (error: Error | null | undefined) => void
|
||||||
|
) => {
|
||||||
|
const callback =
|
||||||
|
typeof encodingOrCb === 'function' ? encodingOrCb : cb
|
||||||
|
if (callback) {
|
||||||
|
callback(new Error('Test Error')) // Simulate an error
|
||||||
|
}
|
||||||
|
return true // Simulate that the write operation was called
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call the writeStream function and catch the error
|
||||||
const error = await writeStream(stream, content).catch((e: any) => e)
|
const error = await writeStream(stream, content).catch((e: any) => e)
|
||||||
|
|
||||||
|
// Assert that the error is correctly handled
|
||||||
expect(error.message).toEqual('Test Error')
|
expect(error.message).toEqual('Test Error')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ import { WriteStream } from '../../types'
|
|||||||
export const writeStream = async (
|
export const writeStream = async (
|
||||||
stream: WriteStream,
|
stream: WriteStream,
|
||||||
content: string
|
content: string
|
||||||
): Promise<void> =>
|
): Promise<boolean> => {
|
||||||
stream.write(content + '\n', (e: any) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (e) return Promise.reject(e)
|
stream.write(content + '\n', (err: Error | null | undefined) => {
|
||||||
|
if (err) {
|
||||||
return Promise.resolve()
|
reject(err) // Reject on write error
|
||||||
|
} else {
|
||||||
|
resolve(true) // Resolve on successful write
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { extractUserLongNameSas9 } from '../utils/sas9/extractUserLongNameSas9'
|
|||||||
import { openWebPage } from './openWebPage'
|
import { openWebPage } from './openWebPage'
|
||||||
import { verifySas9Login } from './verifySas9Login'
|
import { verifySas9Login } from './verifySas9Login'
|
||||||
import { verifySasViyaLogin } from './verifySasViyaLogin'
|
import { verifySasViyaLogin } from './verifySasViyaLogin'
|
||||||
|
import { isLogInSuccessHeaderPresent } from './'
|
||||||
|
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
public userName = ''
|
public userName = ''
|
||||||
@@ -132,7 +133,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 = isLogInSuccessHeaderPresent(this.serverType, loginResponse)
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
if (isCredentialsVerifyError(loginResponse)) {
|
if (isCredentialsVerifyError(loginResponse)) {
|
||||||
@@ -217,7 +218,7 @@ export class AuthManager {
|
|||||||
* - a boolean `isLoggedIn`
|
* - a boolean `isLoggedIn`
|
||||||
* - a string `userName`,
|
* - a string `userName`,
|
||||||
* - a string `userFullName` and
|
* - a string `userFullName` and
|
||||||
* - a form `loginForm` if not loggedin.
|
* - a form `loginForm` if not loggedIn.
|
||||||
*/
|
*/
|
||||||
public async checkSession(): Promise<LoginResultInternal> {
|
public async checkSession(): Promise<LoginResultInternal> {
|
||||||
const { isLoggedIn, userName, userLongName } = await this.fetchUserName()
|
const { isLoggedIn, userName, userLongName } = await this.fetchUserName()
|
||||||
@@ -384,9 +385,3 @@ const isCredentialsVerifyError = (response: string): boolean =>
|
|||||||
/An error occurred while the system was verifying your credentials. Please enter your credentials again./gm.test(
|
/An error occurred while the system was verifying your credentials. Please enter your credentials again./gm.test(
|
||||||
response
|
response
|
||||||
)
|
)
|
||||||
|
|
||||||
const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
|
|
||||||
if (serverType === ServerType.Sasjs) return response?.loggedin
|
|
||||||
|
|
||||||
return /You have signed in/gm.test(response)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './AuthManager'
|
export * from './AuthManager'
|
||||||
export * from './isAuthorizeFormRequired'
|
export * from './isAuthorizeFormRequired'
|
||||||
export * from './isLoginRequired'
|
export * from './isLoginRequired'
|
||||||
|
export * from './loginHeader'
|
||||||
|
|||||||
97
src/auth/loginHeader.ts
Normal file
97
src/auth/loginHeader.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import { getUserLanguage } from '../utils'
|
||||||
|
|
||||||
|
const enLoginSuccessHeader = 'You have signed in.'
|
||||||
|
|
||||||
|
export const defaultSuccessHeaderKey = 'default'
|
||||||
|
|
||||||
|
// The following headers provided by https://github.com/sasjs/adapter/issues/835#issuecomment-2177818601
|
||||||
|
export const loginSuccessHeaders: { [key: string]: string } = {
|
||||||
|
es: `Ya se ha iniciado la sesi\u00f3n.`,
|
||||||
|
th: `\u0e04\u0e38\u0e13\u0e25\u0e07\u0e0a\u0e37\u0e48\u0e2d\u0e40\u0e02\u0e49\u0e32\u0e43\u0e0a\u0e49\u0e41\u0e25\u0e49\u0e27`,
|
||||||
|
ja: `\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3057\u305f\u3002`,
|
||||||
|
nb: `Du har logget deg p\u00e5.`,
|
||||||
|
sl: `Prijavili ste se.`,
|
||||||
|
ar: `\u0644\u0642\u062f \u0642\u0645\u062a `,
|
||||||
|
sk: `Prihl\u00e1sili ste sa.`,
|
||||||
|
zh_HK: `\u60a8\u5df2\u767b\u5165\u3002`,
|
||||||
|
zh_CN: `\u60a8\u5df2\u767b\u5f55\u3002`,
|
||||||
|
it: `L'utente si \u00e8 connesso.`,
|
||||||
|
sv: `Du har loggat in.`,
|
||||||
|
he: `\u05e0\u05db\u05e0\u05e1\u05ea `,
|
||||||
|
nl: `U hebt zich aangemeld.`,
|
||||||
|
pl: `Zosta\u0142e\u015b zalogowany.`,
|
||||||
|
ko: `\ub85c\uadf8\uc778\ud588\uc2b5\ub2c8\ub2e4.`,
|
||||||
|
zh_TW: `\u60a8\u5df2\u767b\u5165\u3002`,
|
||||||
|
tr: `Oturum a\u00e7t\u0131n\u0131z.`,
|
||||||
|
iw: `\u05e0\u05db\u05e0\u05e1\u05ea `,
|
||||||
|
fr: `Vous \u00eates connect\u00e9.`,
|
||||||
|
uk: `\u0412\u0438 \u0432\u0432\u0456\u0439\u0448\u043b\u0438 \u0432 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441.`,
|
||||||
|
pt_BR: `Voc\u00ea se conectou.`,
|
||||||
|
no: `Du har logget deg p\u00e5.`,
|
||||||
|
cs: `Jste p\u0159ihl\u00e1\u0161eni.`,
|
||||||
|
fi: `Olet kirjautunut sis\u00e4\u00e4n.`,
|
||||||
|
ru: `\u0412\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u043b\u0438 \u0432\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443.`,
|
||||||
|
el: `\u0388\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af.`,
|
||||||
|
hr: `Prijavili ste se.`,
|
||||||
|
da: `Du er logget p\u00e5.`,
|
||||||
|
de: `Sie sind jetzt angemeldet.`,
|
||||||
|
sh: `Prijavljeni ste.`,
|
||||||
|
pt: `Iniciou sess\u00e3o.`,
|
||||||
|
hu: `Bejelentkezett.`,
|
||||||
|
sr: `Prijavljeni ste.`,
|
||||||
|
en: enLoginSuccessHeader,
|
||||||
|
[defaultSuccessHeaderKey]: enLoginSuccessHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides expected login header based on language settings of the browser.
|
||||||
|
* @returns - expected header as a string.
|
||||||
|
*/
|
||||||
|
export const getExpectedLogInSuccessHeader = (): string => {
|
||||||
|
// get default success header
|
||||||
|
let successHeader = loginSuccessHeaders[defaultSuccessHeaderKey]
|
||||||
|
|
||||||
|
// get user language based on language settings of the browser
|
||||||
|
const userLang = getUserLanguage()
|
||||||
|
|
||||||
|
if (userLang) {
|
||||||
|
// get success header on exact match of the language code
|
||||||
|
let userLangSuccessHeader = loginSuccessHeaders[userLang]
|
||||||
|
|
||||||
|
// handle case when there is no exact match of the language code
|
||||||
|
if (!userLangSuccessHeader) {
|
||||||
|
// get all supported language codes
|
||||||
|
const headerLanguages = Object.keys(loginSuccessHeaders)
|
||||||
|
|
||||||
|
// find language code on partial match
|
||||||
|
const headerLanguage = headerLanguages.find((language) =>
|
||||||
|
new RegExp(language, 'i').test(userLang)
|
||||||
|
)
|
||||||
|
|
||||||
|
// reassign success header if partial match was found
|
||||||
|
if (headerLanguage) {
|
||||||
|
successHeader = loginSuccessHeaders[headerLanguage]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
successHeader = userLangSuccessHeader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return successHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if Login success header is present in the response based on language settings of the browser.
|
||||||
|
* @param serverType - server type.
|
||||||
|
* @param response - response object.
|
||||||
|
* @returns - boolean indicating if Login success header is present.
|
||||||
|
*/
|
||||||
|
export const isLogInSuccessHeaderPresent = (
|
||||||
|
serverType: ServerType,
|
||||||
|
response: any
|
||||||
|
): boolean => {
|
||||||
|
if (serverType === ServerType.Sasjs) return response?.loggedIn
|
||||||
|
|
||||||
|
return new RegExp(getExpectedLogInSuccessHeader(), 'gm').test(response)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SasAuthResponse, ServerType } from '@sasjs/utils/types'
|
import { SasAuthResponse, ServerType } from '@sasjs/utils/types'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { isNode } from '../utils'
|
import { isNode } from '../utils'
|
||||||
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
|
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
import { AuthManager } from '../AuthManager'
|
import { AuthManager } from '../AuthManager'
|
||||||
import * as dotenv from 'dotenv'
|
import * as dotenv from 'dotenv'
|
||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {
|
import {
|
||||||
mockedCurrentUserApi,
|
mockedCurrentUserApi,
|
||||||
mockLoginAuthoriseRequiredResponse,
|
mockLoginAuthoriseRequiredResponse
|
||||||
mockLoginSuccessResponse
|
|
||||||
} from './mockResponses'
|
} from './mockResponses'
|
||||||
import { serialize } from '../../utils'
|
import { serialize } from '../../utils'
|
||||||
import * as openWebPageModule from '../openWebPage'
|
import * as openWebPageModule from '../openWebPage'
|
||||||
import * as verifySasViyaLoginModule from '../verifySasViyaLogin'
|
import * as verifySasViyaLoginModule from '../verifySasViyaLogin'
|
||||||
import * as verifySas9LoginModule from '../verifySas9Login'
|
import * as verifySas9LoginModule from '../verifySas9Login'
|
||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
|
import { getExpectedLogInSuccessHeader } from '../'
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||||
|
|
||||||
@@ -125,6 +130,7 @@ describe('AuthManager', () => {
|
|||||||
requestClient,
|
requestClient,
|
||||||
authCallback
|
authCallback
|
||||||
)
|
)
|
||||||
|
|
||||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
@@ -133,8 +139,9 @@ describe('AuthManager', () => {
|
|||||||
loginForm: { name: 'test' }
|
loginForm: { name: 'test' }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mockedAxios.post.mockImplementation(() =>
|
mockedAxios.post.mockImplementation(() =>
|
||||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
Promise.resolve({ data: getExpectedLogInSuccessHeader() })
|
||||||
)
|
)
|
||||||
|
|
||||||
const loginResponse = await authManager.logIn(userName, password)
|
const loginResponse = await authManager.logIn(userName, password)
|
||||||
@@ -152,7 +159,7 @@ describe('AuthManager', () => {
|
|||||||
`/SASLogon/login`,
|
`/SASLogon/login`,
|
||||||
loginParams,
|
loginParams,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
Accept: '*/*'
|
Accept: '*/*'
|
||||||
@@ -170,6 +177,7 @@ describe('AuthManager', () => {
|
|||||||
requestClient,
|
requestClient,
|
||||||
authCallback
|
authCallback
|
||||||
)
|
)
|
||||||
|
|
||||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
@@ -178,8 +186,9 @@ describe('AuthManager', () => {
|
|||||||
loginForm: { name: 'test' }
|
loginForm: { name: 'test' }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mockedAxios.post.mockImplementation(() =>
|
mockedAxios.post.mockImplementation(() =>
|
||||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
Promise.resolve({ data: getExpectedLogInSuccessHeader() })
|
||||||
)
|
)
|
||||||
mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 }))
|
mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 }))
|
||||||
|
|
||||||
@@ -198,7 +207,7 @@ describe('AuthManager', () => {
|
|||||||
`/SASLogon/login`,
|
`/SASLogon/login`,
|
||||||
loginParams,
|
loginParams,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
Accept: '*/*'
|
Accept: '*/*'
|
||||||
@@ -247,7 +256,7 @@ describe('AuthManager', () => {
|
|||||||
`/SASLogon/login`,
|
`/SASLogon/login`,
|
||||||
loginParams,
|
loginParams,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
Accept: '*/*'
|
Accept: '*/*'
|
||||||
@@ -530,7 +539,7 @@ describe('AuthManager', () => {
|
|||||||
1,
|
1,
|
||||||
`http://test-server.com/identities/users/@currentUser`,
|
`http://test-server.com/identities/users/@currentUser`,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
transformResponse: undefined,
|
transformResponse: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -564,7 +573,7 @@ describe('AuthManager', () => {
|
|||||||
1,
|
1,
|
||||||
`http://test-server.com/SASStoredProcess`,
|
`http://test-server.com/SASStoredProcess`,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
transformResponse: undefined,
|
transformResponse: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -593,7 +602,7 @@ describe('AuthManager', () => {
|
|||||||
1,
|
1,
|
||||||
`http://test-server.com/identities/users/@currentUser`,
|
`http://test-server.com/identities/users/@currentUser`,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
transformResponse: undefined,
|
transformResponse: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -612,7 +621,7 @@ describe('AuthManager', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const getHeadersJson = {
|
const getHeadersJson = {
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AuthConfig } from '@sasjs/utils/types'
|
import { AuthConfig } from '@sasjs/utils/types'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import { getAccessTokenForViya } from '../getAccessTokenForViya'
|
import { getAccessTokenForViya } from '../getAccessTokenForViya'
|
||||||
|
|||||||
82
src/auth/spec/loginHeader.spec.ts
Normal file
82
src/auth/spec/loginHeader.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import {
|
||||||
|
loginSuccessHeaders,
|
||||||
|
isLogInSuccessHeaderPresent,
|
||||||
|
defaultSuccessHeaderKey
|
||||||
|
} from '../'
|
||||||
|
|
||||||
|
describe('isLogInSuccessHeaderPresent', () => {
|
||||||
|
let languageGetter: any
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
languageGetter = jest.spyOn(window.navigator, 'language', 'get')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should check SASVIYA and SAS9 login success header based on language preferences of the browser', () => {
|
||||||
|
// test SASVIYA server type
|
||||||
|
Object.keys(loginSuccessHeaders).forEach((key) => {
|
||||||
|
languageGetter.mockReturnValue(key)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isLogInSuccessHeaderPresent(
|
||||||
|
ServerType.SasViya,
|
||||||
|
loginSuccessHeaders[key]
|
||||||
|
)
|
||||||
|
).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
// test SAS9 server type
|
||||||
|
Object.keys(loginSuccessHeaders).forEach((key) => {
|
||||||
|
languageGetter.mockReturnValue(key)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isLogInSuccessHeaderPresent(ServerType.Sas9, loginSuccessHeaders[key])
|
||||||
|
).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
// test possible longer language codes
|
||||||
|
const possibleLanguageCodes = [
|
||||||
|
{ short: 'en', long: 'en-US' },
|
||||||
|
{ short: 'fr', long: 'fr-FR' },
|
||||||
|
{ short: 'es', long: 'es-ES' }
|
||||||
|
]
|
||||||
|
|
||||||
|
possibleLanguageCodes.forEach((key) => {
|
||||||
|
const { short, long } = key
|
||||||
|
languageGetter.mockReturnValue(long)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isLogInSuccessHeaderPresent(
|
||||||
|
ServerType.SasViya,
|
||||||
|
loginSuccessHeaders[short]
|
||||||
|
)
|
||||||
|
).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
// test falling back to default language code
|
||||||
|
languageGetter.mockReturnValue('WRONG-LANGUAGE')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isLogInSuccessHeaderPresent(
|
||||||
|
ServerType.Sas9,
|
||||||
|
loginSuccessHeaders[defaultSuccessHeaderKey]
|
||||||
|
)
|
||||||
|
).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should check SASVJS login success header', () => {
|
||||||
|
expect(
|
||||||
|
isLogInSuccessHeaderPresent(ServerType.Sasjs, { loggedIn: true })
|
||||||
|
).toBeTruthy()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isLogInSuccessHeaderPresent(ServerType.Sasjs, { loggedIn: false })
|
||||||
|
).toBeFalsy()
|
||||||
|
|
||||||
|
expect(isLogInSuccessHeaderPresent(ServerType.Sasjs, undefined)).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { SasAuthResponse } from '@sasjs/utils/types'
|
import { SasAuthResponse } from '@sasjs/utils/types'
|
||||||
|
|
||||||
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
||||||
export const mockLoginSuccessResponse = `You have signed in`
|
|
||||||
|
|
||||||
export const mockAuthResponse: SasAuthResponse = {
|
export const mockAuthResponse: SasAuthResponse = {
|
||||||
access_token: 'acc355',
|
access_token: 'acc355',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import { refreshTokensForViya } from '../refreshTokensForViya'
|
import { refreshTokensForViya } from '../refreshTokensForViya'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { verifySas9Login } from '../verifySas9Login'
|
import { verifySas9Login } from '../verifySas9Login'
|
||||||
import * as delayModule from '../../utils/delay'
|
import * as delayModule from '../../utils/delay'
|
||||||
|
import { getExpectedLogInSuccessHeader } from '../'
|
||||||
|
|
||||||
describe('verifySas9Login', () => {
|
describe('verifySas9Login', () => {
|
||||||
const serverUrl = 'http://test-server.com'
|
const serverUrl = 'http://test-server.com'
|
||||||
@@ -18,7 +19,9 @@ describe('verifySas9Login', () => {
|
|||||||
const popup = {
|
const popup = {
|
||||||
window: {
|
window: {
|
||||||
location: { href: serverUrl + `/SASLogon` },
|
location: { href: serverUrl + `/SASLogon` },
|
||||||
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
|
document: {
|
||||||
|
body: { innerText: `<h3>${getExpectedLogInSuccessHeader()}</h3>` }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as unknown as Window
|
} as unknown as Window
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { verifySasViyaLogin } from '../verifySasViyaLogin'
|
import { verifySasViyaLogin } from '../verifySasViyaLogin'
|
||||||
import * as delayModule from '../../utils/delay'
|
import * as delayModule from '../../utils/delay'
|
||||||
|
import { getExpectedLogInSuccessHeader } from '../'
|
||||||
|
|
||||||
describe('verifySasViyaLogin', () => {
|
describe('verifySasViyaLogin', () => {
|
||||||
const serverUrl = 'http://test-server.com'
|
const serverUrl = 'http://test-server.com'
|
||||||
@@ -19,7 +20,9 @@ describe('verifySasViyaLogin', () => {
|
|||||||
const popup = {
|
const popup = {
|
||||||
window: {
|
window: {
|
||||||
location: { href: serverUrl + `/SASLogon` },
|
location: { href: serverUrl + `/SASLogon` },
|
||||||
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
|
document: {
|
||||||
|
body: { innerText: `<h3>${getExpectedLogInSuccessHeader()}</h3>` }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as unknown as Window
|
} as unknown as Window
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { delay } from '../utils'
|
import { delay } from '../utils'
|
||||||
|
import { getExpectedLogInSuccessHeader } from './'
|
||||||
|
|
||||||
export async function verifySas9Login(loginPopup: Window): Promise<{
|
export async function verifySas9Login(loginPopup: Window): Promise<{
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
@@ -6,13 +7,17 @@ export async function verifySas9Login(loginPopup: Window): Promise<{
|
|||||||
let isLoggedIn = false
|
let isLoggedIn = false
|
||||||
let startTime = new Date()
|
let startTime = new Date()
|
||||||
let elapsedSeconds = 0
|
let elapsedSeconds = 0
|
||||||
|
|
||||||
do {
|
do {
|
||||||
await delay(1000)
|
await delay(1000)
|
||||||
if (loginPopup.closed) break
|
if (loginPopup.closed) break
|
||||||
|
|
||||||
isLoggedIn =
|
isLoggedIn =
|
||||||
loginPopup.window.location.href.includes('SASLogon') &&
|
loginPopup.window.location.href.includes('SASLogon') &&
|
||||||
loginPopup.window.document.body.innerText.includes('You have signed in.')
|
loginPopup.window.document.body.innerText.includes(
|
||||||
|
getExpectedLogInSuccessHeader()
|
||||||
|
)
|
||||||
|
|
||||||
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
||||||
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
|
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { delay } from '../utils'
|
import { delay } from '../utils'
|
||||||
|
import { getExpectedLogInSuccessHeader } from './'
|
||||||
|
|
||||||
export async function verifySasViyaLogin(loginPopup: Window): Promise<{
|
export async function verifySasViyaLogin(loginPopup: Window): Promise<{
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
@@ -6,23 +7,32 @@ export async function verifySasViyaLogin(loginPopup: Window): Promise<{
|
|||||||
let isLoggedIn = false
|
let isLoggedIn = false
|
||||||
let startTime = new Date()
|
let startTime = new Date()
|
||||||
let elapsedSeconds = 0
|
let elapsedSeconds = 0
|
||||||
|
|
||||||
do {
|
do {
|
||||||
await delay(1000)
|
await delay(1000)
|
||||||
|
|
||||||
if (loginPopup.closed) break
|
if (loginPopup.closed) break
|
||||||
|
|
||||||
isLoggedIn = isLoggedInSASVIYA()
|
isLoggedIn = isLoggedInSASVIYA()
|
||||||
|
|
||||||
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
||||||
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
|
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
|
||||||
|
|
||||||
let isAuthorized = false
|
let isAuthorized = false
|
||||||
|
|
||||||
startTime = new Date()
|
startTime = new Date()
|
||||||
|
|
||||||
do {
|
do {
|
||||||
await delay(1000)
|
await delay(1000)
|
||||||
|
|
||||||
if (loginPopup.closed) break
|
if (loginPopup.closed) break
|
||||||
|
|
||||||
isAuthorized =
|
isAuthorized =
|
||||||
loginPopup.window.location.href.includes('SASLogon') ||
|
loginPopup.window.location.href.includes('SASLogon') ||
|
||||||
loginPopup.window.document.body?.innerText?.includes(
|
loginPopup.window.document.body?.innerText?.includes(
|
||||||
'You have signed in.'
|
getExpectedLogInSuccessHeader()
|
||||||
)
|
)
|
||||||
|
|
||||||
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
||||||
} while (!isAuthorized && elapsedSeconds < 5 * 60)
|
} while (!isAuthorized && elapsedSeconds < 5 * 60)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { convertToCSV } from '../utils/convertToCsv'
|
import { convertToCSV } from '../utils/convertToCsv'
|
||||||
import { isNode } from '../utils'
|
import { isNode } from '../utils'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { convertToCSV, isFormatsTable } from '../utils/convertToCsv'
|
import { convertToCSV, isFormatsTable } from '../utils/convertToCsv'
|
||||||
import { splitChunks } from '../utils/splitChunks'
|
import { splitChunks } from '../utils/splitChunks'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateFileUploadForm } from '../generateFileUploadForm'
|
import { generateFileUploadForm } from '../generateFileUploadForm'
|
||||||
import { convertToCSV } from '../../utils/convertToCsv'
|
import { convertToCSV } from '../../utils/convertToCsv'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import * as isNodeModule from '../../utils/isNode'
|
import * as isNodeModule from '../../utils/isNode'
|
||||||
|
|
||||||
describe('generateFileUploadForm', () => {
|
describe('generateFileUploadForm', () => {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export abstract class BaseJobExecutor implements JobExecutor {
|
|||||||
|
|
||||||
if (config.debug) {
|
if (config.debug) {
|
||||||
requestParams['_omittextlog'] = 'false'
|
requestParams['_omittextlog'] = 'false'
|
||||||
requestParams['_omitsessionresults'] = 'false'
|
requestParams['_omitSessionResults'] = 'false'
|
||||||
|
|
||||||
requestParams['_debug'] = 131
|
requestParams['_debug'] = 131
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import { ErrorResponse } from '../types/errors'
|
import { ErrorResponse } from '../types/errors'
|
||||||
import { convertToCSV, isRelativePath } from '../utils'
|
import { convertToCSV, isRelativePath } from '../utils'
|
||||||
import { BaseJobExecutor } from './JobExecutor'
|
import { BaseJobExecutor } from './JobExecutor'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import {
|
import {
|
||||||
AuthConfig,
|
AuthConfig,
|
||||||
ExtraResponseAttributes,
|
ExtraResponseAttributes,
|
||||||
@@ -73,8 +73,10 @@ export class SasjsJobExecutor extends BaseJobExecutor {
|
|||||||
/* The NodeFormData object does not set the request header - so, set it */
|
/* The NodeFormData object does not set the request header - so, set it */
|
||||||
const contentType =
|
const contentType =
|
||||||
formData instanceof NodeFormData && typeof FormData === 'undefined'
|
formData instanceof NodeFormData && typeof FormData === 'undefined'
|
||||||
? `multipart/form-data; boundary=${formData.getBoundary()}`
|
? `multipart/form-data; boundary=${
|
||||||
: undefined
|
formData.getHeaders()['content-type']
|
||||||
|
}`
|
||||||
|
: 'multipart/form-data'
|
||||||
|
|
||||||
const requestPromise = new Promise((resolve, reject) => {
|
const requestPromise = new Promise((resolve, reject) => {
|
||||||
this.requestClient!.post(
|
this.requestClient!.post(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
import {
|
import {
|
||||||
AuthConfig,
|
AuthConfig,
|
||||||
ExtraResponseAttributes,
|
ExtraResponseAttributes,
|
||||||
@@ -150,8 +150,10 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
/* The NodeFormData object does not set the request header - so, set it */
|
/* The NodeFormData object does not set the request header - so, set it */
|
||||||
const contentType =
|
const contentType =
|
||||||
formData instanceof NodeFormData && typeof FormData === 'undefined'
|
formData instanceof NodeFormData && typeof FormData === 'undefined'
|
||||||
? `multipart/form-data; boundary=${formData.getBoundary()}`
|
? `multipart/form-data; boundary=${
|
||||||
: undefined
|
formData.getHeaders()['content-type']
|
||||||
|
}`
|
||||||
|
: 'multipart/form-data'
|
||||||
|
|
||||||
const requestPromise = new Promise((resolve, reject) => {
|
const requestPromise = new Promise((resolve, reject) => {
|
||||||
this.requestClient!.post(
|
this.requestClient!.post(
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
AxiosError,
|
AxiosError,
|
||||||
AxiosInstance,
|
AxiosInstance,
|
||||||
AxiosRequestConfig,
|
AxiosRequestConfig,
|
||||||
|
AxiosRequestHeaders,
|
||||||
AxiosResponse
|
AxiosResponse
|
||||||
} from 'axios'
|
} from 'axios'
|
||||||
import axios from 'axios'
|
|
||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { CsrfToken } from '..'
|
import { CsrfToken } from '..'
|
||||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||||
@@ -160,7 +160,7 @@ export class RequestClient implements HttpClient {
|
|||||||
const requestConfig: AxiosRequestConfig = {
|
const requestConfig: AxiosRequestConfig = {
|
||||||
headers,
|
headers,
|
||||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||||
withCredentials: true
|
withXSRFToken: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentType === 'text/plain') {
|
if (contentType === 'text/plain') {
|
||||||
@@ -191,6 +191,13 @@ export class RequestClient implements HttpClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param contentType Newer version of Axios is more strict so if you don't
|
||||||
|
* set the contentType to `form data` while sending a FormData object
|
||||||
|
* application/json will be used by default, axios won’t treat it as FormData.
|
||||||
|
* Instead, it serializes data as JSON—resulting in a payload like
|
||||||
|
* {"sometable":{}} and we lose the multipart/form-data formatting.
|
||||||
|
*/
|
||||||
public async post<T>(
|
public async post<T>(
|
||||||
url: string,
|
url: string,
|
||||||
data: any,
|
data: any,
|
||||||
@@ -207,7 +214,7 @@ export class RequestClient implements HttpClient {
|
|||||||
return this.httpClient
|
return this.httpClient
|
||||||
.post<T>(url, data, {
|
.post<T>(url, data, {
|
||||||
headers,
|
headers,
|
||||||
withCredentials: true,
|
withXSRFToken: true,
|
||||||
...additionalSettings
|
...additionalSettings
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -234,7 +241,7 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.put<T>(url, data, { headers, withCredentials: true })
|
.put<T>(url, data, { headers, withXSRFToken: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
@@ -253,7 +260,7 @@ export class RequestClient implements HttpClient {
|
|||||||
const headers = this.getHeaders(accessToken, 'application/json')
|
const headers = this.getHeaders(accessToken, 'application/json')
|
||||||
|
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.delete<T>(url, { headers, withCredentials: true })
|
.delete<T>(url, { headers, withXSRFToken: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
@@ -271,7 +278,7 @@ export class RequestClient implements HttpClient {
|
|||||||
const headers = this.getHeaders(accessToken, 'application/json')
|
const headers = this.getHeaders(accessToken, 'application/json')
|
||||||
|
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.patch<T>(url, data, { headers, withCredentials: true })
|
.patch<T>(url, data, { headers, withXSRFToken: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
@@ -413,95 +420,17 @@ export class RequestClient implements HttpClient {
|
|||||||
return bodyLines.join('\n')
|
return bodyLines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
private defaultInterceptionCallBack = (
|
private handleAxiosResponse = (response: AxiosResponse) => {
|
||||||
axiosResponse: AxiosResponse | AxiosError
|
const { status, config, request, data } = response
|
||||||
) => {
|
|
||||||
// Message indicating absent value.
|
|
||||||
const noValueMessage = 'Not provided'
|
|
||||||
|
|
||||||
// Fallback request object that can be safely used to form request summary.
|
const reqHeaders = request?._header ?? 'Not provided\n'
|
||||||
type FallbackRequest = { _header?: string; res: { rawHeaders: string[] } }
|
const rawHeaders = request?.res?.rawHeaders ?? ['Not provided']
|
||||||
// _header is not present in responses with status 1**
|
|
||||||
// rawHeaders are not present in responses with status 1**
|
|
||||||
let fallbackRequest: FallbackRequest = {
|
|
||||||
_header: `${noValueMessage}\n`,
|
|
||||||
res: { rawHeaders: [noValueMessage] }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback response object that can be safely used to form response summary.
|
const resHeaders = this.formatHeaders(rawHeaders)
|
||||||
type FallbackResponse = {
|
const parsedResBody = this.parseInterceptedBody(data)
|
||||||
status?: number | string
|
|
||||||
request?: FallbackRequest
|
|
||||||
config: { data?: string }
|
|
||||||
data?: unknown
|
|
||||||
}
|
|
||||||
let fallbackResponse: FallbackResponse = axiosResponse
|
|
||||||
|
|
||||||
if (axios.isAxiosError(axiosResponse)) {
|
|
||||||
const { response, request, config } = axiosResponse
|
|
||||||
|
|
||||||
// Try to use axiosResponse.response to form response summary.
|
|
||||||
if (response) {
|
|
||||||
fallbackResponse = response
|
|
||||||
} else {
|
|
||||||
// Try to use axiosResponse.request to form request summary.
|
|
||||||
if (request) {
|
|
||||||
const { _header, _currentRequest } = request
|
|
||||||
|
|
||||||
// Try to use axiosResponse.request._header to form request summary.
|
|
||||||
if (_header) {
|
|
||||||
fallbackRequest._header = _header
|
|
||||||
}
|
|
||||||
// Try to use axiosResponse.request._currentRequest._header to form request summary.
|
|
||||||
else if (_currentRequest && _currentRequest._header) {
|
|
||||||
fallbackRequest._header = _currentRequest._header
|
|
||||||
}
|
|
||||||
|
|
||||||
const { res } = request
|
|
||||||
|
|
||||||
// Try to use axiosResponse.request.res.rawHeaders to form request summary.
|
|
||||||
if (res && res.rawHeaders) {
|
|
||||||
fallbackRequest.res.rawHeaders = res.rawHeaders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback config that can be safely used to form response summary.
|
|
||||||
const fallbackConfig = { data: noValueMessage }
|
|
||||||
|
|
||||||
fallbackResponse = {
|
|
||||||
status: noValueMessage,
|
|
||||||
request: fallbackRequest,
|
|
||||||
config: config || fallbackConfig,
|
|
||||||
data: noValueMessage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, config, request, data: resData } = fallbackResponse
|
|
||||||
const { data: reqData } = config
|
|
||||||
const { _header: reqHeaders, res } = request || fallbackRequest
|
|
||||||
const { rawHeaders } = res
|
|
||||||
|
|
||||||
// Converts an array of strings into a single string with the following format:
|
|
||||||
// <headerName>: <headerValue>
|
|
||||||
const resHeaders = rawHeaders.reduce(
|
|
||||||
(acc: string, value: string, i: number) => {
|
|
||||||
if (i % 2 === 0) {
|
|
||||||
acc += `${i === 0 ? '' : '\n'}${value}`
|
|
||||||
} else {
|
|
||||||
acc += `: ${value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
''
|
|
||||||
)
|
|
||||||
|
|
||||||
const parsedResBody = this.parseInterceptedBody(resData)
|
|
||||||
|
|
||||||
// HTTP response summary.
|
|
||||||
process.logger?.info(`HTTP Request (first 50 lines):
|
process.logger?.info(`HTTP Request (first 50 lines):
|
||||||
${reqHeaders}${this.parseInterceptedBody(reqData)}
|
${reqHeaders}${this.parseInterceptedBody(config.data)}
|
||||||
|
|
||||||
HTTP Response Code: ${this.prettifyString(status)}
|
HTTP Response Code: ${this.prettifyString(status)}
|
||||||
|
|
||||||
@@ -509,7 +438,70 @@ HTTP Response (first 50 lines):
|
|||||||
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
return axiosResponse
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAxiosError = (error: AxiosError) => {
|
||||||
|
// Message indicating absent value.
|
||||||
|
const noValueMessage = 'Not provided'
|
||||||
|
const { response, request, config } = error
|
||||||
|
|
||||||
|
// Fallback request object that can be safely used to form request summary.
|
||||||
|
// _header is not present in responses with status 1**
|
||||||
|
// rawHeaders are not present in responses with status 1**
|
||||||
|
let fallbackRequest = {
|
||||||
|
_header: `${noValueMessage}\n`,
|
||||||
|
res: { rawHeaders: [noValueMessage] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request) {
|
||||||
|
fallbackRequest = {
|
||||||
|
_header:
|
||||||
|
request._header ?? request._currentRequest?._header ?? noValueMessage,
|
||||||
|
res: { rawHeaders: request.res?.rawHeaders ?? [noValueMessage] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback response object that can be safely used to form response summary.
|
||||||
|
let fallbackResponse = response || {
|
||||||
|
status: noValueMessage,
|
||||||
|
request: fallbackRequest,
|
||||||
|
config: config || {
|
||||||
|
data: noValueMessage,
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
},
|
||||||
|
data: noValueMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, request: req, data: resData } = fallbackResponse
|
||||||
|
const { _header: reqHeaders, res } = req
|
||||||
|
|
||||||
|
const resHeaders = this.formatHeaders(res.rawHeaders)
|
||||||
|
const parsedResBody = this.parseInterceptedBody(resData)
|
||||||
|
|
||||||
|
process.logger?.info(`HTTP Request (first 50 lines):
|
||||||
|
${reqHeaders}${this.parseInterceptedBody(config?.data)}
|
||||||
|
|
||||||
|
HTTP Response Code: ${this.prettifyString(status)}
|
||||||
|
|
||||||
|
HTTP Response (first 50 lines):
|
||||||
|
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
||||||
|
`)
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts an array of strings into a single string with the following format:
|
||||||
|
// <headerName>: <headerValue>
|
||||||
|
private formatHeaders = (rawHeaders: string[]): string => {
|
||||||
|
return rawHeaders.reduce((acc, value, i) => {
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
acc += `${i === 0 ? '' : '\n'}${value}`
|
||||||
|
} else {
|
||||||
|
acc += `: ${value}`
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -529,8 +521,8 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
||||||
*/
|
*/
|
||||||
public enableVerboseMode = (
|
public enableVerboseMode = (
|
||||||
successCallBack = this.defaultInterceptionCallBack,
|
successCallBack = this.handleAxiosResponse,
|
||||||
errorCallBack = this.defaultInterceptionCallBack
|
errorCallBack = this.handleAxiosError
|
||||||
) => {
|
) => {
|
||||||
this.httpInterceptor = this.httpClient.interceptors.response.use(
|
this.httpInterceptor = this.httpClient.interceptors.response.use(
|
||||||
successCallBack,
|
successCallBack,
|
||||||
@@ -645,7 +637,7 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
// Fetching root and creating CSRF cookie
|
// Fetching root and creating CSRF cookie
|
||||||
await this.httpClient
|
await this.httpClient
|
||||||
.get('/', {
|
.get('/', {
|
||||||
withCredentials: true
|
withXSRFToken: true
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const cookie =
|
const cookie =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { AxiosRequestConfig } from 'axios'
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import axiosCookieJarSupport from 'axios-cookiejar-support'
|
import { wrapper } from 'axios-cookiejar-support'
|
||||||
import * as tough from 'tough-cookie'
|
import * as tough from 'tough-cookie'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { RequestClient, throwIfError } from './RequestClient'
|
import { RequestClient, throwIfError } from './RequestClient'
|
||||||
@@ -17,8 +17,8 @@ export class Sas9RequestClient extends RequestClient {
|
|||||||
this.httpClient.defaults.validateStatus = (status) =>
|
this.httpClient.defaults.validateStatus = (status) =>
|
||||||
status >= 200 && status < 303
|
status >= 200 && status < 303
|
||||||
|
|
||||||
if (axiosCookieJarSupport) {
|
if (wrapper) {
|
||||||
axiosCookieJarSupport(this.httpClient)
|
wrapper(this.httpClient)
|
||||||
this.httpClient.defaults.jar = new tough.CookieJar()
|
this.httpClient.defaults.jar = new tough.CookieJar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export class Sas9RequestClient extends RequestClient {
|
|||||||
const requestConfig: AxiosRequestConfig = {
|
const requestConfig: AxiosRequestConfig = {
|
||||||
headers,
|
headers,
|
||||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||||
withCredentials: true
|
withXSRFToken: true
|
||||||
}
|
}
|
||||||
if (contentType === 'text/plain') {
|
if (contentType === 'text/plain') {
|
||||||
requestConfig.transformResponse = undefined
|
requestConfig.transformResponse = undefined
|
||||||
@@ -103,7 +103,7 @@ export class Sas9RequestClient extends RequestClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.post<T>(url, data, { headers, withCredentials: true })
|
.post<T>(url, data, { headers, withXSRFToken: true })
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (response.status === 302) {
|
if (response.status === 302) {
|
||||||
return await this.get(
|
return await this.get(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SASJS_LOGS_SEPARATOR, SasjsRequestClient } from '../SasjsRequestClient'
|
import { SASJS_LOGS_SEPARATOR, SasjsRequestClient } from '../SasjsRequestClient'
|
||||||
import { SasjsParsedResponse } from '../../types'
|
import { SasjsParsedResponse } from '../../types'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosRequestHeaders, AxiosResponse } from 'axios'
|
||||||
|
|
||||||
describe('SasjsRequestClient', () => {
|
describe('SasjsRequestClient', () => {
|
||||||
const requestClient = new SasjsRequestClient('')
|
const requestClient = new SasjsRequestClient('')
|
||||||
@@ -37,7 +37,9 @@ ${SASJS_LOGS_SEPARATOR}`,
|
|||||||
status,
|
status,
|
||||||
statusText: 'ok',
|
statusText: 'ok',
|
||||||
headers: { etag },
|
headers: { etag },
|
||||||
config: {}
|
config: {
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||||
@@ -65,7 +67,9 @@ ${printOutput}`,
|
|||||||
status,
|
status,
|
||||||
statusText: 'ok',
|
statusText: 'ok',
|
||||||
headers: { etag },
|
headers: { etag },
|
||||||
config: {}
|
config: {
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||||
@@ -100,7 +104,9 @@ ${SASJS_LOGS_SEPARATOR}`,
|
|||||||
status,
|
status,
|
||||||
statusText: 'ok',
|
statusText: 'ok',
|
||||||
headers: { etag },
|
headers: { etag },
|
||||||
config: {}
|
config: {
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||||
@@ -139,7 +145,9 @@ ${printOutput}`,
|
|||||||
status,
|
status,
|
||||||
statusText: 'ok',
|
statusText: 'ok',
|
||||||
headers: { etag },
|
headers: { etag },
|
||||||
config: {}
|
config: {
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||||
|
|||||||
130
src/spec/SAS9ApiClient.spec.ts
Normal file
130
src/spec/SAS9ApiClient.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
import * as https from 'https'
|
||||||
|
import NodeFormData from 'form-data'
|
||||||
|
import { SAS9ApiClient } from '../SAS9ApiClient'
|
||||||
|
import { Sas9RequestClient } from '../request/Sas9RequestClient'
|
||||||
|
|
||||||
|
// Mock the Sas9RequestClient so that we can control its behavior
|
||||||
|
jest.mock('../request/Sas9RequestClient', () => {
|
||||||
|
return {
|
||||||
|
Sas9RequestClient: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(serverUrl: string, httpsAgentOptions?: https.AgentOptions) => {
|
||||||
|
return {
|
||||||
|
login: jest.fn().mockResolvedValue(undefined),
|
||||||
|
post: jest.fn().mockResolvedValue({ result: 'execution result' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SAS9ApiClient', () => {
|
||||||
|
const serverUrl = 'http://test-server.com'
|
||||||
|
const jobsPath = '/SASStoredProcess/do'
|
||||||
|
let client: SAS9ApiClient
|
||||||
|
let mockRequestClient: any
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new SAS9ApiClient(serverUrl, jobsPath)
|
||||||
|
// Retrieve the instance of the mocked Sas9RequestClient
|
||||||
|
mockRequestClient = (Sas9RequestClient as jest.Mock).mock.results[0].value
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getConfig', () => {
|
||||||
|
it('should return the correct configuration', () => {
|
||||||
|
const config = client.getConfig()
|
||||||
|
expect(config).toEqual({ serverUrl })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setConfig', () => {
|
||||||
|
it('should update the serverUrl when a valid value is provided', () => {
|
||||||
|
const newUrl = 'http://new-server.com'
|
||||||
|
client.setConfig(newUrl)
|
||||||
|
expect(client.getConfig()).toEqual({ serverUrl: newUrl })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not update the serverUrl when an empty string is provided', () => {
|
||||||
|
const originalConfig = client.getConfig()
|
||||||
|
client.setConfig('')
|
||||||
|
expect(client.getConfig()).toEqual(originalConfig)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('executeScript', () => {
|
||||||
|
const linesOfCode = ['line1;', 'line2;']
|
||||||
|
const userName = 'testUser'
|
||||||
|
const password = 'testPass'
|
||||||
|
const fixedTimestamp = '1234567890'
|
||||||
|
const expectedFilename = `sasjs-execute-sas9-${fixedTimestamp}.sas`
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Stub generateTimestamp so that we get a consistent filename in our tests.
|
||||||
|
jest
|
||||||
|
.spyOn(require('@sasjs/utils/time'), 'generateTimestamp')
|
||||||
|
.mockReturnValue(fixedTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute the script and return the result', async () => {
|
||||||
|
const result = await client.executeScript(linesOfCode, userName, password)
|
||||||
|
|
||||||
|
// Verify that login is called with the correct parameters.
|
||||||
|
expect(mockRequestClient.login).toHaveBeenCalledWith(
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
jobsPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build the expected stored process URL.
|
||||||
|
const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
|
||||||
|
const expectedUrl =
|
||||||
|
`${jobsPath}/?` + '_program=' + codeInjectorPath + '&_debug=log'
|
||||||
|
|
||||||
|
// Verify that post was called with the expected stored process URL.
|
||||||
|
expect(mockRequestClient.post).toHaveBeenCalledWith(
|
||||||
|
expectedUrl,
|
||||||
|
expect.any(NodeFormData),
|
||||||
|
undefined,
|
||||||
|
expect.stringContaining('multipart/form-data; boundary='),
|
||||||
|
expect.objectContaining({
|
||||||
|
'Content-Length': expect.any(Number),
|
||||||
|
'Content-Type': expect.stringContaining(
|
||||||
|
'multipart/form-data; boundary='
|
||||||
|
),
|
||||||
|
Accept: '*/*'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// The method should return the result from the post call.
|
||||||
|
expect(result).toEqual('execution result')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include the force output code in the uploaded form data', async () => {
|
||||||
|
await client.executeScript(linesOfCode, userName, password)
|
||||||
|
// Retrieve the form data passed to post
|
||||||
|
const postCallArgs = (mockRequestClient.post as jest.Mock).mock.calls[0]
|
||||||
|
const formData: NodeFormData = postCallArgs[1]
|
||||||
|
|
||||||
|
// We can inspect the boundary and ensure that the filename was generated correctly.
|
||||||
|
expect(formData.getBoundary()).toBeDefined()
|
||||||
|
|
||||||
|
// The filename is used as the key for the form field.
|
||||||
|
const formDataBuffer = formData.getBuffer().toString()
|
||||||
|
expect(formDataBuffer).toContain(expectedFilename)
|
||||||
|
// Also check that the force output code is appended.
|
||||||
|
expect(formDataBuffer).toContain("put 'Executed sasjs run';")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
231
src/spec/SASjsApiClient.spec.ts
Normal file
231
src/spec/SASjsApiClient.spec.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import NodeFormData from 'form-data'
|
||||||
|
import {
|
||||||
|
SASjsApiClient,
|
||||||
|
SASjsAuthResponse,
|
||||||
|
ScriptExecutionResult
|
||||||
|
} from '../SASjsApiClient'
|
||||||
|
import { AuthConfig, ServicePackSASjs } from '@sasjs/utils/types'
|
||||||
|
import { ExecutionQuery } from '../types'
|
||||||
|
|
||||||
|
// Create a mock request client with a post method.
|
||||||
|
const mockPost = jest.fn()
|
||||||
|
const mockRequestClient = {
|
||||||
|
post: mockPost
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of referencing external variables, inline the dummy values in the mock factories.
|
||||||
|
jest.mock('../auth/getTokens', () => ({
|
||||||
|
getTokens: jest.fn().mockResolvedValue({ access_token: 'dummyAccessToken' })
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../auth/getAccessTokenForSasjs', () => ({
|
||||||
|
getAccessTokenForSasjs: jest.fn().mockResolvedValue({
|
||||||
|
access_token: 'newAccessToken',
|
||||||
|
refresh_token: 'newRefreshToken'
|
||||||
|
} as any)
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../auth/refreshTokensForSasjs', () => ({
|
||||||
|
refreshTokensForSasjs: jest.fn().mockResolvedValue({
|
||||||
|
access_token: 'newAccessToken',
|
||||||
|
refresh_token: 'newRefreshToken'
|
||||||
|
} as any)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// For deployZipFile, mock the file reading function.
|
||||||
|
jest.mock('@sasjs/utils/file', () => ({
|
||||||
|
createReadStream: jest.fn().mockResolvedValue('readStreamDummy')
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dummy result to compare against.
|
||||||
|
const dummyResult = {
|
||||||
|
status: 'OK',
|
||||||
|
message: 'Success',
|
||||||
|
streamServiceName: 'service',
|
||||||
|
example: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SASjsApiClient', () => {
|
||||||
|
let client: SASjsApiClient
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new SASjsApiClient(mockRequestClient as any)
|
||||||
|
mockPost.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deploy', () => {
|
||||||
|
it('should deploy service pack using JSON', async () => {
|
||||||
|
// Arrange: Simulate a successful response.
|
||||||
|
mockPost.mockResolvedValue({ result: dummyResult })
|
||||||
|
|
||||||
|
const dataJson: ServicePackSASjs = {
|
||||||
|
appLoc: '',
|
||||||
|
someOtherProp: 'value'
|
||||||
|
} as any
|
||||||
|
const appLoc = '/base/appLoc'
|
||||||
|
const authConfig: AuthConfig = {
|
||||||
|
client: 'clientId',
|
||||||
|
secret: 'secret',
|
||||||
|
access_token: 'token',
|
||||||
|
refresh_token: 'refresh'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await client.deploy(dataJson, appLoc, authConfig)
|
||||||
|
|
||||||
|
// Assert: Ensure that the JSON gets the appLoc set if not defined.
|
||||||
|
expect(dataJson.appLoc).toBe(appLoc)
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'SASjsApi/drive/deploy',
|
||||||
|
dataJson,
|
||||||
|
'dummyAccessToken',
|
||||||
|
undefined,
|
||||||
|
{},
|
||||||
|
{ maxContentLength: Infinity, maxBodyLength: Infinity }
|
||||||
|
)
|
||||||
|
expect(result).toEqual(dummyResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deployZipFile', () => {
|
||||||
|
it('should deploy zip file and return the result', async () => {
|
||||||
|
// Arrange: Simulate a successful response.
|
||||||
|
mockPost.mockResolvedValue({ result: dummyResult })
|
||||||
|
const zipFilePath = 'path/to/deploy.zip'
|
||||||
|
const authConfig: AuthConfig = {
|
||||||
|
client: 'clientId',
|
||||||
|
secret: 'secret',
|
||||||
|
access_token: 'token',
|
||||||
|
refresh_token: 'refresh'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await client.deployZipFile(zipFilePath, authConfig)
|
||||||
|
|
||||||
|
// Assert: Verify that POST is called with multipart form-data.
|
||||||
|
expect(mockPost).toHaveBeenCalled()
|
||||||
|
const callArgs = mockPost.mock.calls[0]
|
||||||
|
expect(callArgs[0]).toBe('SASjsApi/drive/deploy/upload')
|
||||||
|
expect(result).toEqual(dummyResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('executeJob', () => {
|
||||||
|
it('should execute a job with absolute program path', async () => {
|
||||||
|
// Arrange
|
||||||
|
const query: ExecutionQuery = { _program: '/absolute/path' } as any
|
||||||
|
const appLoc = '/base/appLoc'
|
||||||
|
const authConfig: AuthConfig = { access_token: 'anyToken' } as any
|
||||||
|
mockPost.mockResolvedValue({
|
||||||
|
result: { jobId: 123 },
|
||||||
|
log: 'execution log'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { result, log } = await client.executeJob(query, appLoc, authConfig)
|
||||||
|
|
||||||
|
// Assert: The program path should not be prefixed.
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'SASjsApi/stp/execute',
|
||||||
|
{ _debug: 131, ...query, _program: '/absolute/path' },
|
||||||
|
'anyToken'
|
||||||
|
)
|
||||||
|
expect(result).toEqual({ jobId: 123 })
|
||||||
|
expect(log).toBe('execution log')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute a job with relative program path', async () => {
|
||||||
|
// Arrange
|
||||||
|
const query: ExecutionQuery = { _program: 'relative/path' } as any
|
||||||
|
const appLoc = '/base/appLoc'
|
||||||
|
mockPost.mockResolvedValue({ result: { jobId: 456 }, log: 'another log' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { result, log } = await client.executeJob(query, appLoc)
|
||||||
|
|
||||||
|
// Assert: The program path should be prefixed with appLoc.
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'SASjsApi/stp/execute',
|
||||||
|
{ _debug: 131, ...query, _program: '/base/appLoc/relative/path' },
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
expect(result).toEqual({ jobId: 456 })
|
||||||
|
expect(log).toBe('another log')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('executeScript', () => {
|
||||||
|
it('should execute a script and return the execution result', async () => {
|
||||||
|
// Arrange
|
||||||
|
const code = 'data _null_; run;'
|
||||||
|
const runTime = 'sas'
|
||||||
|
const authConfig: AuthConfig = {
|
||||||
|
client: 'clientId',
|
||||||
|
secret: 'secret',
|
||||||
|
access_token: 'token',
|
||||||
|
refresh_token: 'refresh'
|
||||||
|
}
|
||||||
|
const responsePayload = {
|
||||||
|
log: 'log output',
|
||||||
|
printOutput: 'print output',
|
||||||
|
result: 'web output'
|
||||||
|
}
|
||||||
|
mockPost.mockResolvedValue(responsePayload)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result: ScriptExecutionResult = await client.executeScript(
|
||||||
|
code,
|
||||||
|
runTime,
|
||||||
|
authConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'SASjsApi/code/execute',
|
||||||
|
{ code, runTime },
|
||||||
|
'dummyAccessToken'
|
||||||
|
)
|
||||||
|
expect(result.log).toBe('log output')
|
||||||
|
expect(result.printOutput).toBe('print output')
|
||||||
|
expect(result.webout).toBe('web output')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error with a prefixed message when POST fails', async () => {
|
||||||
|
// Arrange
|
||||||
|
const code = 'data _null_; run;'
|
||||||
|
const errorMessage = 'Network Error'
|
||||||
|
mockPost.mockRejectedValue(new Error(errorMessage))
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(client.executeScript(code)).rejects.toThrow(
|
||||||
|
/Error while sending POST request to execute code/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getAccessToken', () => {
|
||||||
|
it('should exchange auth code for access token', async () => {
|
||||||
|
// Act
|
||||||
|
const result = await client.getAccessToken('clientId', 'authCode123')
|
||||||
|
|
||||||
|
// Assert: The result should match the dummy auth response.
|
||||||
|
expect(result).toEqual({
|
||||||
|
access_token: 'newAccessToken',
|
||||||
|
refresh_token: 'newRefreshToken'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('refreshTokens', () => {
|
||||||
|
it('should exchange refresh token for new tokens', async () => {
|
||||||
|
// Act
|
||||||
|
const result = await client.refreshTokens('refreshToken123')
|
||||||
|
|
||||||
|
// Assert: The result should match the dummy auth response.
|
||||||
|
expect(result).toEqual({
|
||||||
|
access_token: 'newAccessToken',
|
||||||
|
refresh_token: 'newRefreshToken'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,7 +5,7 @@ import { app, mockedAuthResponse } from './SAS_server_app'
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import SASjs from '../SASjs'
|
import SASjs from '../SASjs'
|
||||||
import * as axiosModules from '../utils/createAxiosInstance'
|
import * as axiosModules from '../utils/createAxiosInstance'
|
||||||
import axios from 'axios'
|
import axios, { AxiosRequestHeaders } from 'axios'
|
||||||
import {
|
import {
|
||||||
LoginRequiredError,
|
LoginRequiredError,
|
||||||
AuthorizeError,
|
AuthorizeError,
|
||||||
@@ -24,9 +24,17 @@ const axiosActual = jest.requireActual('axios')
|
|||||||
jest
|
jest
|
||||||
.spyOn(axiosModules, 'createAxiosInstance')
|
.spyOn(axiosModules, 'createAxiosInstance')
|
||||||
.mockImplementation((baseURL: string, httpsAgent?: https.Agent) =>
|
.mockImplementation((baseURL: string, httpsAgent?: https.Agent) =>
|
||||||
axiosActual.create({ baseURL, httpsAgent })
|
axiosActual.create({ baseURL, httpsAgent, withXSRFToken: true })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
jest.mock('util', () => {
|
||||||
|
const actualUtil = jest.requireActual('util')
|
||||||
|
return {
|
||||||
|
...actualUtil,
|
||||||
|
inspect: jest.fn(actualUtil.inspect)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const PORT = 8000
|
const PORT = 8000
|
||||||
const SERVER_URL = `https://localhost:${PORT}/`
|
const SERVER_URL = `https://localhost:${PORT}/`
|
||||||
|
|
||||||
@@ -75,7 +83,7 @@ describe('RequestClient', () => {
|
|||||||
expect(rejectionErrorMessage).toEqual(expectedError.message)
|
expect(rejectionErrorMessage).toEqual(expectedError.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('defaultInterceptionCallBack', () => {
|
describe('defaultInterceptionCallBacks for successful requests and failed requests', () => {
|
||||||
const reqHeaders = `POST https://sas.server.com/compute/sessions/session_id/jobs HTTP/1.1
|
const reqHeaders = `POST https://sas.server.com/compute/sessions/session_id/jobs HTTP/1.1
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -96,7 +104,7 @@ Connection: close
|
|||||||
_contextName: 'SAS Job Execution compute context',
|
_contextName: 'SAS Job Execution compute context',
|
||||||
_OMITJSONLISTING: true,
|
_OMITJSONLISTING: true,
|
||||||
_OMITJSONLOG: true,
|
_OMITJSONLOG: true,
|
||||||
_OMITSESSIONRESULTS: true,
|
_omitSessionResults: true,
|
||||||
_OMITTEXTLISTING: true,
|
_OMITTEXTLISTING: true,
|
||||||
_OMITTEXTLOG: true
|
_OMITTEXTLOG: true
|
||||||
}
|
}
|
||||||
@@ -165,10 +173,6 @@ Connection: close
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should log parsed response with status 1**', () => {
|
it('should log parsed response with status 1**', () => {
|
||||||
const spyIsAxiosError = jest
|
|
||||||
.spyOn(axios, 'isAxiosError')
|
|
||||||
.mockImplementation(() => true)
|
|
||||||
|
|
||||||
const mockedAxiosError = {
|
const mockedAxiosError = {
|
||||||
config: {
|
config: {
|
||||||
data: reqData
|
data: reqData
|
||||||
@@ -181,7 +185,7 @@ Connection: close
|
|||||||
} as AxiosError
|
} as AxiosError
|
||||||
|
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
requestClient['handleAxiosError'](mockedAxiosError)
|
||||||
|
|
||||||
const noValueMessage = 'Not provided'
|
const noValueMessage = 'Not provided'
|
||||||
const expectedLog = `HTTP Request (first 50 lines):
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
@@ -195,8 +199,6 @@ ${noValueMessage}
|
|||||||
`
|
`
|
||||||
|
|
||||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||||
|
|
||||||
spyIsAxiosError.mockReset()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should log parsed response with status 2**', () => {
|
it('should log parsed response with status 2**', () => {
|
||||||
@@ -209,12 +211,15 @@ ${noValueMessage}
|
|||||||
status,
|
status,
|
||||||
statusText: '',
|
statusText: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
config: { data: reqData },
|
config: {
|
||||||
|
data: reqData,
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
},
|
||||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
requestClient['defaultInterceptionCallBack'](mockedResponse)
|
requestClient['handleAxiosResponse'](mockedResponse)
|
||||||
|
|
||||||
const expectedLog = `HTTP Request (first 50 lines):
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
@@ -235,29 +240,29 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
it('should log parsed response with status 3**', () => {
|
it('should log parsed response with status 3**', () => {
|
||||||
const status = getRandomStatus([300, 301, 302, 303, 304, 307, 308])
|
const status = getRandomStatus([300, 301, 302, 303, 304, 307, 308])
|
||||||
|
|
||||||
const mockedResponse: AxiosResponse = {
|
const mockedAxiosError = {
|
||||||
data: resData,
|
config: {
|
||||||
status,
|
data: reqData
|
||||||
statusText: '',
|
},
|
||||||
headers: {},
|
request: {
|
||||||
config: { data: reqData },
|
_currentRequest: {
|
||||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
_header: reqHeaders
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} as AxiosError
|
||||||
|
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
requestClient['defaultInterceptionCallBack'](mockedResponse)
|
requestClient['handleAxiosError'](mockedAxiosError)
|
||||||
|
|
||||||
|
const noValueMessage = 'Not provided'
|
||||||
const expectedLog = `HTTP Request (first 50 lines):
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
|
|
||||||
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
HTTP Response Code: ${requestClient['prettifyString'](noValueMessage)}
|
||||||
|
|
||||||
HTTP Response (first 50 lines):
|
HTTP Response (first 50 lines):
|
||||||
${resHeaders[0]}: ${resHeaders[1]}${
|
${noValueMessage}
|
||||||
requestClient['parseInterceptedBody'](resData)
|
\n${requestClient['parseInterceptedBody'](noValueMessage)}
|
||||||
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||||
@@ -278,7 +283,10 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
status,
|
status,
|
||||||
statusText: '',
|
statusText: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
config: { data: reqData },
|
config: {
|
||||||
|
data: reqData,
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
},
|
||||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||||
}
|
}
|
||||||
const mockedAxiosError = {
|
const mockedAxiosError = {
|
||||||
@@ -294,7 +302,7 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
} as AxiosError
|
} as AxiosError
|
||||||
|
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
requestClient['handleAxiosError'](mockedAxiosError)
|
||||||
|
|
||||||
const expectedLog = `HTTP Request (first 50 lines):
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
@@ -328,7 +336,10 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
status,
|
status,
|
||||||
statusText: '',
|
statusText: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
config: { data: reqData },
|
config: {
|
||||||
|
data: reqData,
|
||||||
|
headers: {} as AxiosRequestHeaders
|
||||||
|
},
|
||||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||||
}
|
}
|
||||||
const mockedAxiosError = {
|
const mockedAxiosError = {
|
||||||
@@ -344,7 +355,7 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
} as AxiosError
|
} as AxiosError
|
||||||
|
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
requestClient['handleAxiosError'](mockedAxiosError)
|
||||||
|
|
||||||
const expectedLog = `HTTP Request (first 50 lines):
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
@@ -376,8 +387,8 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
requestClient.enableVerboseMode()
|
requestClient.enableVerboseMode()
|
||||||
|
|
||||||
expect(interceptorSpy).toHaveBeenCalledWith(
|
expect(interceptorSpy).toHaveBeenCalledWith(
|
||||||
requestClient['defaultInterceptionCallBack'],
|
requestClient['handleAxiosResponse'],
|
||||||
requestClient['defaultInterceptionCallBack']
|
requestClient['handleAxiosError']
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -388,12 +399,12 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
'use'
|
'use'
|
||||||
)
|
)
|
||||||
|
|
||||||
const successCallback = (response: AxiosResponse | AxiosError) => {
|
const successCallback = (response: AxiosResponse) => {
|
||||||
console.log('success')
|
console.log('success')
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
const failureCallback = (response: AxiosResponse | AxiosError) => {
|
const failureCallback = (response: AxiosError) => {
|
||||||
console.log('failure')
|
console.log('failure')
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -429,15 +440,18 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('prettifyString', () => {
|
describe('prettifyString', () => {
|
||||||
|
const inspectMock = UtilsModule.inspect as unknown as jest.Mock
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset the mock before each test to ensure a clean slate
|
||||||
|
inspectMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
it(`should call inspect without colors when verbose mode is set to 'bleached'`, () => {
|
it(`should call inspect without colors when verbose mode is set to 'bleached'`, () => {
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
let verbose: VerboseMode = 'bleached'
|
requestClient.setVerboseMode('bleached')
|
||||||
requestClient.setVerboseMode(verbose)
|
|
||||||
|
|
||||||
jest.spyOn(UtilsModule, 'inspect')
|
|
||||||
|
|
||||||
const testStr = JSON.stringify({ test: 'test' })
|
const testStr = JSON.stringify({ test: 'test' })
|
||||||
|
|
||||||
requestClient['prettifyString'](testStr)
|
requestClient['prettifyString'](testStr)
|
||||||
|
|
||||||
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
||||||
@@ -445,15 +459,11 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it(`should call inspect with colors when verbose mode is set to 'true'`, () => {
|
it(`should call inspect with colors when verbose mode is set to true`, () => {
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
let verbose: VerboseMode = true
|
requestClient.setVerboseMode(true)
|
||||||
requestClient.setVerboseMode(verbose)
|
|
||||||
|
|
||||||
jest.spyOn(UtilsModule, 'inspect')
|
|
||||||
|
|
||||||
const testStr = JSON.stringify({ test: 'test' })
|
const testStr = JSON.stringify({ test: 'test' })
|
||||||
|
|
||||||
requestClient['prettifyString'](testStr)
|
requestClient['prettifyString'](testStr)
|
||||||
|
|
||||||
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import express = require('express')
|
import express = require('express')
|
||||||
|
import cors from 'cors'
|
||||||
|
|
||||||
export const app = express()
|
export const app = express()
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: 'http://localhost', // Allow requests only from this origin
|
||||||
|
credentials: true // Allow credentials (cookies, auth headers, etc.)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export const mockedAuthResponse = {
|
export const mockedAuthResponse = {
|
||||||
access_token: 'access_token',
|
access_token: 'access_token',
|
||||||
token_type: 'bearer',
|
token_type: 'bearer',
|
||||||
@@ -12,11 +20,11 @@ export const mockedAuthResponse = {
|
|||||||
jti: 'jti'
|
jti: 'jti'
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get('/', function (req: any, res: any) {
|
app.get('/', (req: any, res: any) => {
|
||||||
res.send('Hello World')
|
res.send('Hello World')
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/SASLogon/oauth/token', function (req: any, res: any) {
|
app.post('/SASLogon/oauth/token', (req: any, res: any) => {
|
||||||
let valid = true
|
let valid = true
|
||||||
|
|
||||||
// capture the encoded form data
|
// capture the encoded form data
|
||||||
|
|||||||
33
src/types/FileResource.ts
Normal file
33
src/types/FileResource.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export interface FileResource {
|
||||||
|
creationTimeStamp: string
|
||||||
|
modifiedTimeStamp: string
|
||||||
|
createdBy: string
|
||||||
|
modifiedBy: string
|
||||||
|
id: string
|
||||||
|
properties: Properties
|
||||||
|
contentDisposition: string
|
||||||
|
contentType: string
|
||||||
|
encoding: string
|
||||||
|
links: Link[]
|
||||||
|
name: string
|
||||||
|
size: number
|
||||||
|
searchable: boolean
|
||||||
|
fileStatus: string
|
||||||
|
fileVersion: number
|
||||||
|
typeDefName: string
|
||||||
|
version: number
|
||||||
|
virusDetected: boolean
|
||||||
|
urlDetected: boolean
|
||||||
|
quarantine: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Link {
|
||||||
|
method: string
|
||||||
|
rel: string
|
||||||
|
href: string
|
||||||
|
uri: string
|
||||||
|
type?: string
|
||||||
|
responseType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Properties {}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
export interface WriteStream {
|
import { WriteStream as FsWriteStream } from 'fs'
|
||||||
write: (content: string, callback: (err?: Error) => any) => void
|
|
||||||
path: string
|
export interface WriteStream extends FsWriteStream {
|
||||||
|
write(
|
||||||
|
chunk: any,
|
||||||
|
encoding?: BufferEncoding | ((error: Error | null | undefined) => void),
|
||||||
|
cb?: (error: Error | null | undefined) => void
|
||||||
|
): boolean
|
||||||
|
path: string | Buffer
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/types/errors/spec/SAS9AuthError.spec.ts
Normal file
30
src/types/errors/spec/SAS9AuthError.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { SAS9AuthError } from '../SAS9AuthError'
|
||||||
|
|
||||||
|
describe('SAS9AuthError', () => {
|
||||||
|
it('should have the correct error message', () => {
|
||||||
|
const error = new SAS9AuthError()
|
||||||
|
expect(error.message).toBe(
|
||||||
|
'The credentials you provided cannot be authenticated. Please provide a valid set of credentials.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have the correct error name', () => {
|
||||||
|
const error = new SAS9AuthError()
|
||||||
|
expect(error.name).toBe('AuthorizeError')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be an instance of SAS9AuthError', () => {
|
||||||
|
const error = new SAS9AuthError()
|
||||||
|
expect(error).toBeInstanceOf(SAS9AuthError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be an instance of Error', () => {
|
||||||
|
const error = new SAS9AuthError()
|
||||||
|
expect(error).toBeInstanceOf(Error)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set the prototype correctly', () => {
|
||||||
|
const error = new SAS9AuthError()
|
||||||
|
expect(Object.getPrototypeOf(error)).toBe(SAS9AuthError.prototype)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -10,10 +10,14 @@ export const convertToCSV = (
|
|||||||
tableName: string
|
tableName: string
|
||||||
) => {
|
) => {
|
||||||
if (!data[tableName]) {
|
if (!data[tableName]) {
|
||||||
throw prefixMessage(
|
const error = prefixMessage(
|
||||||
'No table provided to be converted to CSV.',
|
'No table provided to be converted to CSV.',
|
||||||
'Error while converting to CSV. '
|
'Error while converting to CSV. '
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (typeof error === 'string') throw new Error(error)
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = data[tableName]
|
const table = data[tableName]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isNode } from './'
|
import { isNode } from './'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
|
|
||||||
export const getFormData = () =>
|
export const getFormData = (): NodeFormData | FormData =>
|
||||||
isNode() ? new NodeFormData() : new FormData()
|
isNode() ? new NodeFormData() : new FormData()
|
||||||
|
|||||||
10
src/utils/getUserLanguage.ts
Normal file
10
src/utils/getUserLanguage.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
interface IEnavigator {
|
||||||
|
userLanguage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides preferred language of the user.
|
||||||
|
* @returns A string representing the preferred language of the user, usually the language of the browser UI. Examples of valid language codes include "en", "en-US", "fr", "fr-FR", "es-ES". More info available https://datatracker.ietf.org/doc/html/rfc5646
|
||||||
|
*/
|
||||||
|
export const getUserLanguage = () =>
|
||||||
|
window.navigator.language || (window.navigator as IEnavigator).userLanguage
|
||||||
@@ -20,3 +20,4 @@ export * from './serialize'
|
|||||||
export * from './splitChunks'
|
export * from './splitChunks'
|
||||||
export * from './validateInput'
|
export * from './validateInput'
|
||||||
export * from './getFormData'
|
export * from './getFormData'
|
||||||
|
export * from './getUserLanguage'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getFormData } from '..'
|
import { getFormData } from '..'
|
||||||
import * as isNodeModule from '../isNode'
|
import * as isNodeModule from '../isNode'
|
||||||
import * as NodeFormData from 'form-data'
|
import NodeFormData from 'form-data'
|
||||||
|
|
||||||
describe('getFormData', () => {
|
describe('getFormData', () => {
|
||||||
it('should return NodeFormData if environment is Node', () => {
|
it('should return NodeFormData if environment is Node', () => {
|
||||||
@@ -10,8 +10,8 @@ describe('getFormData', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return FormData if environment is not Node', () => {
|
it('should return FormData if environment is not Node', () => {
|
||||||
const formDataMock = () => {}
|
// Ensure FormData is globally available
|
||||||
;(global as any).FormData = formDataMock
|
;(global as any).FormData = class FormData {}
|
||||||
|
|
||||||
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => false)
|
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => false)
|
||||||
|
|
||||||
|
|||||||
24
src/utils/spec/parseSasViyaLog.spec.ts
Normal file
24
src/utils/spec/parseSasViyaLog.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { parseSasViyaLog } from '../parseSasViyaLog'
|
||||||
|
|
||||||
|
describe('parseSasViyaLog', () => {
|
||||||
|
it('should parse sas viya log if environment is Node', () => {
|
||||||
|
const logResponse = {
|
||||||
|
items: [{ line: 'Line 1' }, { line: 'Line 2' }, { line: 'Line 3' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedLog = 'Line 1\nLine 2\nLine 3'
|
||||||
|
const result = parseSasViyaLog(logResponse)
|
||||||
|
expect(result).toEqual(expectedLog)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle exceptions and return the original logResponse', () => {
|
||||||
|
// Create a logResponse that will cause an error in the mapping process.
|
||||||
|
const logResponse: any = {
|
||||||
|
items: null
|
||||||
|
}
|
||||||
|
// Since logResponse.items is null, the ternary operator returns the else branch.
|
||||||
|
const expectedLog = JSON.stringify(logResponse)
|
||||||
|
const result = parseSasViyaLog(logResponse)
|
||||||
|
expect(result).toEqual(expectedLog)
|
||||||
|
})
|
||||||
|
})
|
||||||
72
src/utils/spec/parseViyaDebugResponse.spec.ts
Normal file
72
src/utils/spec/parseViyaDebugResponse.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
|
import { parseSasViyaDebugResponse } from '../parseViyaDebugResponse'
|
||||||
|
|
||||||
|
describe('parseSasViyaDebugResponse', () => {
|
||||||
|
let requestClient: RequestClient
|
||||||
|
const serverUrl = 'http://test-server.com'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestClient = {
|
||||||
|
get: jest.fn()
|
||||||
|
} as unknown as RequestClient
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extract URL and call get for Viya 3.5 iframe style', async () => {
|
||||||
|
const iframeUrl = '/path/to/log.json'
|
||||||
|
const response = `<html><body><iframe style="width: 99%; height: 500px" src="${iframeUrl}"></iframe></body></html>`
|
||||||
|
const resultData = { message: 'success' }
|
||||||
|
|
||||||
|
// Mock the get method to resolve with an object containing the JSON result as string.
|
||||||
|
;(requestClient.get as jest.Mock).mockResolvedValue({
|
||||||
|
result: JSON.stringify(resultData)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await parseSasViyaDebugResponse(
|
||||||
|
response,
|
||||||
|
requestClient,
|
||||||
|
serverUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(requestClient.get).toHaveBeenCalledWith(
|
||||||
|
serverUrl + iframeUrl,
|
||||||
|
undefined,
|
||||||
|
'text/plain'
|
||||||
|
)
|
||||||
|
expect(result).toEqual(resultData)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extract URL and call get for Viya 4 iframe style', async () => {
|
||||||
|
const iframeUrl = '/another/path/to/log.json'
|
||||||
|
// Note: For Viya 4, the regex splits in such a way that the extracted URL includes an extra starting double-quote.
|
||||||
|
// For example, the URL becomes: '"/another/path/to/log.json'
|
||||||
|
const response = `<html><body><iframe style="width: 99%; height: 500px; background-color:Canvas;" src="${iframeUrl}"></iframe></body></html>`
|
||||||
|
const resultData = { status: 'ok' }
|
||||||
|
|
||||||
|
;(requestClient.get as jest.Mock).mockResolvedValue({
|
||||||
|
result: JSON.stringify(resultData)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await parseSasViyaDebugResponse(
|
||||||
|
response,
|
||||||
|
requestClient,
|
||||||
|
serverUrl
|
||||||
|
)
|
||||||
|
// Expect the extra starting double-quote as per the current implementation.
|
||||||
|
const expectedUrl = serverUrl + `"` + iframeUrl
|
||||||
|
|
||||||
|
expect(requestClient.get).toHaveBeenCalledWith(
|
||||||
|
expectedUrl,
|
||||||
|
undefined,
|
||||||
|
'text/plain'
|
||||||
|
)
|
||||||
|
expect(result).toEqual(resultData)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error if iframe URL is not found', async () => {
|
||||||
|
const response = `<html><body>No iframe here</body></html>`
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
parseSasViyaDebugResponse(response, requestClient, serverUrl)
|
||||||
|
).rejects.toThrow('Unable to find webout file URL.')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -52,19 +52,22 @@ export const validateInput = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of data[key]) {
|
// ES6 is stricter so we had to include the check for the array
|
||||||
if (getType(item) !== 'object') {
|
if (Array.isArray(data[key])) {
|
||||||
return {
|
for (const item of data[key]) {
|
||||||
status: false,
|
if (getType(item) !== 'object') {
|
||||||
msg: `Table ${key} contains invalid structure. ${MORE_INFO}`
|
return {
|
||||||
}
|
status: false,
|
||||||
} else {
|
msg: `Table ${key} contains invalid structure. ${MORE_INFO}`
|
||||||
const attributes = Object.keys(item)
|
}
|
||||||
for (const attribute of attributes) {
|
} else {
|
||||||
if (item[attribute] === undefined) {
|
const attributes = Object.keys(item)
|
||||||
return {
|
for (const attribute of attributes) {
|
||||||
status: false,
|
if (item[attribute] === undefined) {
|
||||||
msg: `A row in table ${key} contains invalid value. Can't assign undefined to ${attribute}.`
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: `A row in table ${key} contains invalid value. Can't assign undefined to ${attribute}.`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ES2018", "DOM", "ES2019.String"],
|
"lib": ["ES2018", "DOM", "ES2019.String"],
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
"typeRoots": ["./node_modules/@types", "./src/types/system"]
|
"typeRoots": ["./node_modules/@types", "./src/types/system"]
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ const defaultPlugins = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const optimization = {
|
const optimization = {
|
||||||
minimize: true,
|
minimize: false,
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new terserPlugin({
|
// new terserPlugin({
|
||||||
parallel: true,
|
// parallel: true,
|
||||||
terserOptions: {}
|
// terserOptions: {}
|
||||||
})
|
// })
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +27,15 @@ const browserConfig = {
|
|||||||
index: './src/index.ts',
|
index: './src/index.ts',
|
||||||
minified_sas9: './src/minified/sas9/index.ts'
|
minified_sas9: './src/minified/sas9/index.ts'
|
||||||
},
|
},
|
||||||
|
externals: {
|
||||||
|
'node:fs': 'node:fs',
|
||||||
|
'node:fs/promises': 'node:fs/promises',
|
||||||
|
'node:path': 'node:path',
|
||||||
|
'node:stream': 'node:stream',
|
||||||
|
'node:url': 'node:url',
|
||||||
|
'node:events': 'node:events',
|
||||||
|
'node:string_decoder': 'node:string_decoder'
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
path: path.resolve(__dirname, 'build'),
|
path: path.resolve(__dirname, 'build'),
|
||||||
@@ -35,6 +44,7 @@ const browserConfig = {
|
|||||||
},
|
},
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
optimization: optimization,
|
optimization: optimization,
|
||||||
|
devtool: 'inline-source-map',
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user