mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-04 11:10:05 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3556eb3903 | |||
| 600e561a45 | |||
| 6a161a05ef | |||
| 8db02012e5 | |||
| a01b1a9feb | |||
|
|
76bf5b88e9 | ||
|
|
a97ac4eaa6 | ||
|
|
37cfea6ca7 | ||
|
|
f74c8aca57 | ||
|
|
77baaabfcd | ||
|
|
510ba771f0 | ||
|
|
6fce65f4c8 | ||
|
|
fe03faa59f | ||
|
|
6272eeda23 | ||
|
|
104d1b88b3 | ||
|
|
0d9ba36de8 | ||
|
|
4e7a845d99 | ||
|
|
716cc513ff | ||
|
|
22edcb0a8e | ||
|
|
aedf5c1734 | ||
|
|
784bd20ee0 | ||
|
|
61db1e0609 | ||
|
|
5c589a6af3 | ||
|
|
275cd6dbd3 | ||
|
|
d874e07889 | ||
|
|
1648cf28d5 | ||
| a4aaeba31c | |||
|
|
6bf68a315c | ||
|
|
c0f78d0c1e | ||
|
|
e0aebc169f | ||
|
|
9a50e5cb63 | ||
|
|
a51923dad7 | ||
|
|
9aee77f0e3 | ||
|
|
c32d037063 | ||
|
|
94f7492c31 | ||
|
|
d29e0a0f57 | ||
|
|
8d7cc11db5 | ||
|
|
28e9d1cc6b | ||
|
|
375cec48ca | ||
| 7d826685f7 | |||
| f42f6bca00 | |||
|
|
4440e5d1f9 | ||
|
|
f484a5a6a1 | ||
|
|
5c74186bab | ||
|
|
ea68c3dff3 | ||
|
|
153b285670 | ||
|
|
f9f4aa5aa6 | ||
|
|
bd02656b3c | ||
|
|
991519a13d | ||
|
|
615c9d012e | ||
|
|
d166231c12 | ||
|
|
4cb150e951 | ||
|
|
fc8598473f | ||
|
|
367e0ae25a | ||
| e6ec51c7eb | |||
|
|
85dde61baf |
4
.github/reviewer-lottery.yml
vendored
4
.github/reviewer-lottery.yml
vendored
@@ -5,7 +5,3 @@ groups:
|
||||
- YuryShkoda
|
||||
- medjedovicm
|
||||
- sabhas
|
||||
- name: SASjs QA
|
||||
reviewers: 1
|
||||
usernames:
|
||||
- VladislavParhomchik
|
||||
|
||||
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
node-version: [lts/hydrogen]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -22,17 +22,17 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
# - name: Check npm audit
|
||||
# run: npm audit --production --audit-level=low
|
||||
- name: Check npm audit
|
||||
run: npm audit --production --audit-level=low
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
# - name: Check code style
|
||||
# run: npm run lint
|
||||
- name: Check code style
|
||||
run: npm run lint
|
||||
|
||||
# - name: Run unit tests
|
||||
# run: npm test
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Build Package
|
||||
run: npm run package:lib
|
||||
@@ -72,27 +72,19 @@ jobs:
|
||||
npm install -g replace-in-files-cli
|
||||
cd sasjs-tests
|
||||
replace-in-files --regex='"@sasjs/adapter".*' --replacement='"@sasjs/adapter":"latest",' ./package.json
|
||||
npm i --legacy-peer-deps
|
||||
npm i
|
||||
replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json
|
||||
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME_DEV }}",' ./public/config.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD_DEV }}",' ./public/config.json
|
||||
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./public/config.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
|
||||
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
|
||||
# npm run update:adapter
|
||||
npm run update:adapter
|
||||
pm2 start --name sasjs-test npm -- start
|
||||
cat ./public/config.json
|
||||
cat ../cypress.json
|
||||
|
||||
- name: Sleep for 10 seconds
|
||||
uses: jakejarvis/wait-action@master
|
||||
with:
|
||||
time: '10s'
|
||||
|
||||
- name: Run cypress on sasjs
|
||||
run: |
|
||||
ss -lntu
|
||||
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
|
||||
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME_DEV }}",' ./cypress.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD_DEV }}",' ./cypress.json
|
||||
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./cypress.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
|
||||
sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
|
||||
|
||||
# For some reason if coverage report action is run before other commands, those commands can't access the directories and files on which they depend on
|
||||
|
||||
2
.github/workflows/generateDocs.yml
vendored
2
.github/workflows/generateDocs.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
node-version: [lts/hydrogen]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
2
.github/workflows/npmpublish.yml
vendored
2
.github/workflows/npmpublish.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
node-version: [lts/hydrogen]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -5,9 +5,6 @@ const testingFinishTimeout = Cypress.env('testingFinishTimeout')
|
||||
|
||||
context('sasjs-tests', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.task('log', 'beforeAll')
|
||||
cy.task('log', `sasjsTestsUrl: ${sasjsTestsUrl}`)
|
||||
|
||||
cy.visit(sasjsTestsUrl)
|
||||
})
|
||||
|
||||
@@ -16,66 +13,35 @@ context('sasjs-tests', function () {
|
||||
})
|
||||
|
||||
it('Should have all tests successfull', (done) => {
|
||||
cy.task('log', `Should have all tests successfull`)
|
||||
|
||||
cy.get('body').then(($body) => {
|
||||
cy.task('log', `22`)
|
||||
cy.wait(1000).then(() => {
|
||||
const startButton = $body.find(
|
||||
'.ui.massive.icon.primary.left.labeled.button'
|
||||
)[0]
|
||||
|
||||
// ui massive icon primary left labeled button
|
||||
|
||||
cy.task('log', `startButton: ${startButton}`)
|
||||
|
||||
if (
|
||||
!startButton ||
|
||||
(startButton && !Cypress.dom.isVisible(startButton))
|
||||
) {
|
||||
cy.task('log', `34`)
|
||||
cy.task('log', `username: ${username}`)
|
||||
cy.task('log', `password: ${password}`)
|
||||
|
||||
const userNameInput = cy.get('input[placeholder="User Name"]')
|
||||
const passwordInput = cy.get('input[placeholder="Password"]')
|
||||
|
||||
cy.task('log', `userNameInput: ${userNameInput}`)
|
||||
cy.task('log', `passwordInput: ${passwordInput}`)
|
||||
|
||||
cy.get('input[placeholder="User Name"]').type(username)
|
||||
cy.get('input[placeholder="Password"]').type(password)
|
||||
const submitBtn = cy.get('.submit-button')
|
||||
|
||||
cy.task('log', `submitBtn: ${submitBtn}`)
|
||||
cy.get('.submit-button').click()
|
||||
}
|
||||
|
||||
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.task('log', `46`)
|
||||
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.task('log', `50`)
|
||||
|
||||
const loadingButton = $body.find(
|
||||
'.ui.massive.loading.primary.button'
|
||||
)[0]
|
||||
|
||||
cy.task('log', `loadingButton: ${loadingButton}`)
|
||||
|
||||
cy.get('.ui.massive.loading.primary.button', {
|
||||
timeout: testingFinishTimeout
|
||||
})
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.task('log', `56`)
|
||||
cy.get('span.icon.failed')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.task('log', `60`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
@@ -85,46 +51,46 @@ context('sasjs-tests', function () {
|
||||
})
|
||||
})
|
||||
|
||||
// it('Should have all tests successfull with debug on', (done) => {
|
||||
// cy.get('body').then(($body) => {
|
||||
// cy.wait(1000).then(() => {
|
||||
// const startButton = $body.find(
|
||||
// '.ui.massive.icon.primary.left.labeled.button'
|
||||
// )[0]
|
||||
it('Should have all tests successfull with debug on', (done) => {
|
||||
cy.get('body').then(($body) => {
|
||||
cy.wait(1000).then(() => {
|
||||
const startButton = $body.find(
|
||||
'.ui.massive.icon.primary.left.labeled.button'
|
||||
)[0]
|
||||
|
||||
// if (
|
||||
// !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()
|
||||
// }
|
||||
if (
|
||||
!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')
|
||||
// .click()
|
||||
// .then(() => {
|
||||
// cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||
// .should('not.exist')
|
||||
// .then(() => {
|
||||
// cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||
// .click()
|
||||
// .then(() => {
|
||||
// cy.get('.ui.massive.loading.primary.button', {
|
||||
// timeout: testingFinishTimeout
|
||||
// })
|
||||
// .should('not.exist')
|
||||
// .then(() => {
|
||||
// cy.get('span.icon.failed')
|
||||
// .should('not.exist')
|
||||
// .then(() => {
|
||||
// done()
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
cy.get('.ui.fitted.toggle.checkbox label')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.loading.primary.button', {
|
||||
timeout: testingFinishTimeout
|
||||
})
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('span.icon.failed')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,11 +39,4 @@ module.exports = (on, config) => {
|
||||
return launchOptions
|
||||
}
|
||||
})
|
||||
|
||||
on('task', {
|
||||
log(message) {
|
||||
console.log(message)
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1"></script>
|
||||
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@4"></script>
|
||||
<script>
|
||||
var sasJs = new SASjs.default({
|
||||
appLoc: "/Public/app/readme"
|
||||
|
||||
84
package-lock.json
generated
84
package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"axios-cookiejar-support": "1.0.1",
|
||||
"form-data": "4.0.0",
|
||||
"https": "1.0.0",
|
||||
"tough-cookie": "4.0.0"
|
||||
"tough-cookie": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/webpack-preprocessor": "5.9.1",
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/pem": "1.9.6",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"copyfiles": "2.4.1",
|
||||
"cp": "0.2.0",
|
||||
"cypress": "7.7.0",
|
||||
@@ -3440,9 +3440,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz",
|
||||
"integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg=="
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
|
||||
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw=="
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "16.0.5",
|
||||
@@ -14110,6 +14110,11 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -14457,6 +14462,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
@@ -15702,22 +15712,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
||||
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.1.2"
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie/node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
@@ -16351,6 +16362,15 @@
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url/node_modules/punycode": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||
@@ -19536,9 +19556,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/tough-cookie": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz",
|
||||
"integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg=="
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
|
||||
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw=="
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "16.0.5",
|
||||
@@ -27552,6 +27572,11 @@
|
||||
"integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==",
|
||||
"dev": true
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -27833,6 +27858,11 @@
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
@@ -28799,19 +28829,20 @@
|
||||
"dev": true
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
||||
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"requires": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.1.2"
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -29269,6 +29300,15 @@
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"dev": true
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"util": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/pem": "1.9.6",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"copyfiles": "2.4.1",
|
||||
"cp": "0.2.0",
|
||||
"cypress": "7.7.0",
|
||||
@@ -82,6 +82,6 @@
|
||||
"axios-cookiejar-support": "1.0.1",
|
||||
"form-data": "4.0.0",
|
||||
"https": "1.0.0",
|
||||
"tough-cookie": "4.0.0"
|
||||
"tough-cookie": "4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
7697
sasjs-tests/package-lock.json
generated
7697
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,15 +4,14 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "4.3.5",
|
||||
"@sasjs/test-framework": "1.5.7",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.1",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react": "^16.0.1",
|
||||
"@types/react-dom": "^16.0.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react": "^16.0.1",
|
||||
"react-dom": "^16.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
@@ -22,7 +21,7 @@
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz --legacy-peer-deps",
|
||||
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
|
||||
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
|
||||
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
|
||||
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
|
||||
@@ -43,6 +42,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "7.0.3"
|
||||
"node-sass": "9.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"userName": "",
|
||||
"password": "",
|
||||
"sasJsConfig": {
|
||||
"serverUrl": "https://sas9.4gl.io",
|
||||
"serverUrl": "",
|
||||
"appLoc": "/Public/app/adapter-tests/services",
|
||||
"serverType": "SASJS",
|
||||
"debug": false,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"$schema": "https://cli.sasjs.io/sasjsconfig-schema.json",
|
||||
"serviceConfig": {
|
||||
"serviceFolders": [
|
||||
"sasjs/common"
|
||||
]
|
||||
"serviceFolders": ["sasjs/common"]
|
||||
},
|
||||
"defaultTarget": "4gl",
|
||||
"targets": [
|
||||
@@ -28,4 +26,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const Login = (): ReactElement<{}> => {
|
||||
const handleSubmit = useCallback(
|
||||
(e: any) => {
|
||||
e.preventDefault()
|
||||
appContext.adapter.logIn(username, password).then((res: any) => {
|
||||
appContext.adapter.logIn(username, password).then((res) => {
|
||||
appContext.setIsLoggedIn(res.isLoggedIn)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -29,6 +29,12 @@ import { executeScript } from './api/viya/executeScript'
|
||||
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
||||
import { refreshTokensForViya } from './auth/refreshTokensForViya'
|
||||
|
||||
interface JobExecutionResult {
|
||||
result?: { result: object }
|
||||
log?: string
|
||||
error?: object
|
||||
}
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API.
|
||||
*
|
||||
@@ -270,7 +276,7 @@ export class SASViyaApiClient {
|
||||
* @param debug - when set to true, the log will be returned.
|
||||
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
||||
* @param waitForResult - when set to true, function will return the session
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||
* @param variables - an object that represents macro variables.
|
||||
*/
|
||||
@@ -621,7 +627,7 @@ export class SASViyaApiClient {
|
||||
* @param accessToken - an optional access token for an authorized user.
|
||||
* @param waitForResult - a boolean indicating if the function should wait for a result.
|
||||
* @param expectWebout - a boolean indicating whether to expect a _webout response.
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||
* @param variables - an object that represents macro variables.
|
||||
*/
|
||||
@@ -732,11 +738,13 @@ export class SASViyaApiClient {
|
||||
debug: boolean,
|
||||
data?: any,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
): Promise<JobExecutionResult> {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(this.requestClient, authConfig))
|
||||
}
|
||||
|
||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||
throw new Error(
|
||||
'Relative paths cannot be used without specifying a root folder name.'
|
||||
@@ -749,6 +757,7 @@ export class SASViyaApiClient {
|
||||
const fullFolderPath = isRelativePath(sasJob)
|
||||
? `${this.rootFolderName}/${folderPath}`
|
||||
: folderPath
|
||||
|
||||
await this.populateFolderMap(fullFolderPath, access_token)
|
||||
|
||||
const jobFolder = this.folderMap.get(fullFolderPath)
|
||||
@@ -765,9 +774,8 @@ export class SASViyaApiClient {
|
||||
files = await this.uploadTables(data, access_token)
|
||||
}
|
||||
|
||||
if (!jobToExecute) {
|
||||
throw new Error(`Job was not found.`)
|
||||
}
|
||||
if (!jobToExecute) throw new Error(`Job was not found.`)
|
||||
|
||||
const jobDefinitionLink = jobToExecute?.links.find(
|
||||
(l) => l.rel === 'getResource'
|
||||
)?.href
|
||||
@@ -807,16 +815,19 @@ export class SASViyaApiClient {
|
||||
jobDefinition,
|
||||
arguments: jobArguments
|
||||
}
|
||||
|
||||
const { result: postedJob } = await this.requestClient.post<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||
postJobRequestBody,
|
||||
access_token
|
||||
)
|
||||
|
||||
const jobStatus = await this.pollJobState(postedJob, authConfig).catch(
|
||||
(err) => {
|
||||
throw prefixMessage(err, 'Error while polling job status. ')
|
||||
}
|
||||
)
|
||||
|
||||
const { result: currentJob } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||
access_token
|
||||
@@ -827,6 +838,7 @@ export class SASViyaApiClient {
|
||||
|
||||
const resultLink = currentJob.results['_webout.json']
|
||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||
|
||||
if (resultLink) {
|
||||
jobResult = await this.requestClient.get<any>(
|
||||
`${this.serverUrl}${resultLink}/content`,
|
||||
@@ -834,11 +846,13 @@ export class SASViyaApiClient {
|
||||
'text/plain'
|
||||
)
|
||||
}
|
||||
|
||||
if (debug && logLink) {
|
||||
log = await this.requestClient
|
||||
.get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
|
||||
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||
}
|
||||
|
||||
if (jobStatus === 'failed') {
|
||||
throw new JobExecutionError(
|
||||
currentJob.error?.errorCode,
|
||||
@@ -846,7 +860,16 @@ export class SASViyaApiClient {
|
||||
log
|
||||
)
|
||||
}
|
||||
return { result: jobResult?.result, log }
|
||||
|
||||
const executionResult: JobExecutionResult = {
|
||||
result: jobResult?.result,
|
||||
log
|
||||
}
|
||||
|
||||
const { error } = currentJob
|
||||
if (error) executionResult.error = error
|
||||
|
||||
return executionResult
|
||||
}
|
||||
|
||||
private async populateFolderMap(folderPath: string, accessToken?: string) {
|
||||
|
||||
@@ -851,7 +851,7 @@ export default class SASjs {
|
||||
* @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
* The access token is not required when the user is authenticated via the browser.
|
||||
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||
* @param variables - an object that represents macro variables.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ import { RequestClient } from '../../request/RequestClient'
|
||||
import { SessionManager } from '../../SessionManager'
|
||||
import { isRelativePath, fetchLogByChunks } from '../../utils'
|
||||
import { formatDataForRequest } from '../../utils/formatDataForRequest'
|
||||
import { pollJobState } from './pollJobState'
|
||||
import { pollJobState, JobState } from './pollJobState'
|
||||
import { uploadTables } from './uploadTables'
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,7 @@ import { uploadTables } from './uploadTables'
|
||||
* @param debug - when set to true, the log will be returned.
|
||||
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
||||
* @param waitForResult - when set to true, function will return the session
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||
* @param variables - an object that represents macro variables.
|
||||
*/
|
||||
@@ -228,7 +228,7 @@ export async function executeScript(
|
||||
)
|
||||
}
|
||||
|
||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||
if (jobStatus === JobState.Failed || jobStatus === JobState.Error) {
|
||||
throw new ComputeJobExecutionError(currentJob, log)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,88 @@
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
import { Job, PollOptions } from '../..'
|
||||
import { Job, PollOptions, PollStrategy } from '../..'
|
||||
import { getTokens } from '../../auth/getTokens'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { JobStatePollError } from '../../types/errors'
|
||||
import { Link, WriteStream } from '../../types'
|
||||
import { delay, isNode } from '../../utils'
|
||||
|
||||
export enum JobState {
|
||||
Completed = 'completed',
|
||||
Running = 'running',
|
||||
Pending = 'pending',
|
||||
Unavailable = 'unavailable',
|
||||
NoState = '',
|
||||
Failed = 'failed',
|
||||
Error = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls job status using default or provided poll options.
|
||||
* @param requestClient - the pre-configured HTTP request client.
|
||||
* @param postedJob - the relative or absolute path to the job.
|
||||
* @param debug - sets the _debug flag in the job arguments.
|
||||
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||
* @param pollOptions - an object containing maxPollCount, pollInterval, streamLog and logFolderPath. It will override the first default poll options in poll strategy if provided.
|
||||
* Example pollOptions:
|
||||
* {
|
||||
* maxPollCount: 200,
|
||||
* pollInterval: 300,
|
||||
* streamLog: true, // optional, equals to false by default.
|
||||
* pollStrategy?: // optional array of poll options that should be applied after 'maxPollCount' of the provided poll options is reached. If not provided the default (see example below) poll strategy will be used.
|
||||
* }
|
||||
* Example pollStrategy (values used from default poll strategy):
|
||||
* [
|
||||
* { maxPollCount: 200, pollInterval: 300 }, // approximately ~2 mins (including time to get response (~300ms))
|
||||
* { maxPollCount: 300, pollInterval: 3000 }, // approximately ~5.5 mins (including time to get response (~300ms))
|
||||
* { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.5 mins (including time to get response (~300ms))
|
||||
* { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
|
||||
* ]
|
||||
* @returns - a promise which resolves with a job state
|
||||
*/
|
||||
export async function pollJobState(
|
||||
requestClient: RequestClient,
|
||||
postedJob: Job,
|
||||
debug: boolean,
|
||||
authConfig?: AuthConfig,
|
||||
pollOptions?: PollOptions
|
||||
) {
|
||||
): Promise<JobState> {
|
||||
const logger = process.logger || console
|
||||
|
||||
let pollInterval = 300
|
||||
let maxPollCount = 1000
|
||||
const streamLog = pollOptions?.streamLog || false
|
||||
|
||||
const defaultPollOptions: PollOptions = {
|
||||
maxPollCount,
|
||||
pollInterval,
|
||||
streamLog: false
|
||||
const defaultPollStrategy: PollStrategy = [
|
||||
{ maxPollCount: 200, pollInterval: 300 },
|
||||
{ maxPollCount: 300, pollInterval: 3000 },
|
||||
{ maxPollCount: 500, pollInterval: 30000 },
|
||||
{ maxPollCount: 3400, pollInterval: 60000 }
|
||||
]
|
||||
|
||||
let pollStrategy: PollStrategy
|
||||
|
||||
if (pollOptions !== undefined) {
|
||||
pollStrategy = [pollOptions]
|
||||
|
||||
let { pollStrategy: providedPollStrategy } = pollOptions
|
||||
|
||||
if (providedPollStrategy !== undefined) {
|
||||
validatePollStrategies(providedPollStrategy)
|
||||
|
||||
// INFO: sort by 'maxPollCount'
|
||||
providedPollStrategy = providedPollStrategy.sort(
|
||||
(strategyA: PollOptions, strategyB: PollOptions) =>
|
||||
strategyA.maxPollCount - strategyB.maxPollCount
|
||||
)
|
||||
|
||||
pollStrategy = [...pollStrategy, ...providedPollStrategy]
|
||||
} else {
|
||||
pollStrategy = [...pollStrategy, ...defaultPollStrategy]
|
||||
}
|
||||
} else {
|
||||
pollStrategy = defaultPollStrategy
|
||||
}
|
||||
|
||||
let defaultPollOptions: PollOptions = pollStrategy.splice(0, 1)[0]
|
||||
|
||||
pollOptions = { ...defaultPollOptions, ...(pollOptions || {}) }
|
||||
|
||||
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
|
||||
@@ -31,10 +90,10 @@ export async function pollJobState(
|
||||
throw new Error(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
let currentState = await getJobState(
|
||||
let currentState: JobState = await getJobState(
|
||||
requestClient,
|
||||
postedJob,
|
||||
'',
|
||||
JobState.NoState,
|
||||
debug,
|
||||
authConfig
|
||||
).catch((err) => {
|
||||
@@ -42,73 +101,71 @@ export async function pollJobState(
|
||||
`Error fetching job state from ${stateLink.href}. Starting poll, assuming job to be running.`,
|
||||
err
|
||||
)
|
||||
return 'unavailable'
|
||||
|
||||
return JobState.Unavailable
|
||||
})
|
||||
|
||||
let pollCount = 0
|
||||
|
||||
if (currentState === 'completed') {
|
||||
if (currentState === JobState.Completed) {
|
||||
return Promise.resolve(currentState)
|
||||
}
|
||||
|
||||
let logFileStream
|
||||
if (pollOptions.streamLog && isNode()) {
|
||||
if (streamLog && isNode()) {
|
||||
const { getFileStream } = require('./getFileStream')
|
||||
|
||||
logFileStream = await getFileStream(postedJob, pollOptions.logFolderPath)
|
||||
}
|
||||
|
||||
// Poll up to the first 100 times with the specified poll interval
|
||||
let result = await doPoll(
|
||||
requestClient,
|
||||
postedJob,
|
||||
currentState,
|
||||
debug,
|
||||
pollCount,
|
||||
pollOptions,
|
||||
authConfig,
|
||||
{
|
||||
...pollOptions,
|
||||
maxPollCount:
|
||||
pollOptions.maxPollCount <= 100 ? pollOptions.maxPollCount : 100
|
||||
},
|
||||
streamLog,
|
||||
logFileStream
|
||||
)
|
||||
|
||||
currentState = result.state
|
||||
pollCount = result.pollCount
|
||||
|
||||
if (!needsRetry(currentState) || pollCount >= pollOptions.maxPollCount) {
|
||||
if (
|
||||
!needsRetry(currentState) ||
|
||||
(pollCount >= pollOptions.maxPollCount && !pollStrategy.length)
|
||||
) {
|
||||
return currentState
|
||||
}
|
||||
|
||||
// If we get to this point, this is a long-running job that needs longer polling.
|
||||
// We will resume polling with a bigger interval of 1 minute
|
||||
let longJobPollOptions: PollOptions = {
|
||||
maxPollCount: 24 * 60,
|
||||
pollInterval: 60000,
|
||||
streamLog: false
|
||||
}
|
||||
if (pollOptions) {
|
||||
longJobPollOptions.streamLog = pollOptions.streamLog
|
||||
longJobPollOptions.logFolderPath = pollOptions.logFolderPath
|
||||
// INFO: If we get to this point, this is a long-running job that needs longer polling.
|
||||
// We will resume polling with a bigger interval according to the next polling strategy
|
||||
while (pollStrategy.length && needsRetry(currentState)) {
|
||||
defaultPollOptions = pollStrategy.splice(0, 1)[0]
|
||||
|
||||
if (pollOptions) {
|
||||
defaultPollOptions.logFolderPath = pollOptions.logFolderPath
|
||||
}
|
||||
|
||||
result = await doPoll(
|
||||
requestClient,
|
||||
postedJob,
|
||||
currentState,
|
||||
debug,
|
||||
pollCount,
|
||||
defaultPollOptions,
|
||||
authConfig,
|
||||
streamLog,
|
||||
logFileStream
|
||||
)
|
||||
|
||||
currentState = result.state
|
||||
pollCount = result.pollCount
|
||||
}
|
||||
|
||||
result = await doPoll(
|
||||
requestClient,
|
||||
postedJob,
|
||||
currentState,
|
||||
debug,
|
||||
pollCount,
|
||||
authConfig,
|
||||
longJobPollOptions,
|
||||
logFileStream
|
||||
)
|
||||
|
||||
currentState = result.state
|
||||
pollCount = result.pollCount
|
||||
|
||||
if (logFileStream) {
|
||||
logFileStream.end()
|
||||
}
|
||||
if (logFileStream) logFileStream.end()
|
||||
|
||||
return currentState
|
||||
}
|
||||
@@ -119,17 +176,13 @@ const getJobState = async (
|
||||
currentState: string,
|
||||
debug: boolean,
|
||||
authConfig?: AuthConfig
|
||||
) => {
|
||||
const stateLink = job.links.find((l: any) => l.rel === 'state')
|
||||
if (!stateLink) {
|
||||
throw new Error(`Job state link was not found.`)
|
||||
}
|
||||
): Promise<JobState> => {
|
||||
const stateLink = job.links.find((l: any) => l.rel === 'state')!
|
||||
|
||||
if (needsRetry(currentState)) {
|
||||
let tokens
|
||||
if (authConfig) {
|
||||
tokens = await getTokens(requestClient, authConfig)
|
||||
}
|
||||
|
||||
if (authConfig) tokens = await getTokens(requestClient, authConfig)
|
||||
|
||||
const { result: jobState } = await requestClient
|
||||
.get<string>(
|
||||
@@ -143,48 +196,38 @@ const getJobState = async (
|
||||
throw new JobStatePollError(job.id, err)
|
||||
})
|
||||
|
||||
return jobState.trim()
|
||||
return jobState.trim() as JobState
|
||||
} else {
|
||||
return currentState
|
||||
return currentState as JobState
|
||||
}
|
||||
}
|
||||
|
||||
const needsRetry = (state: string) =>
|
||||
state === 'running' ||
|
||||
state === '' ||
|
||||
state === 'pending' ||
|
||||
state === 'unavailable'
|
||||
state === JobState.Running ||
|
||||
state === JobState.NoState ||
|
||||
state === JobState.Pending ||
|
||||
state === JobState.Unavailable
|
||||
|
||||
const doPoll = async (
|
||||
requestClient: RequestClient,
|
||||
postedJob: Job,
|
||||
currentState: string,
|
||||
currentState: JobState,
|
||||
debug: boolean,
|
||||
pollCount: number,
|
||||
pollOptions: PollOptions,
|
||||
authConfig?: AuthConfig,
|
||||
pollOptions?: PollOptions,
|
||||
streamLog?: boolean,
|
||||
logStream?: WriteStream
|
||||
): Promise<{ state: string; pollCount: number }> => {
|
||||
let pollInterval = 300
|
||||
let maxPollCount = 1000
|
||||
): Promise<{ state: JobState; pollCount: number }> => {
|
||||
const { maxPollCount, pollInterval } = pollOptions
|
||||
const logger = process.logger || console
|
||||
const stateLink = postedJob.links.find((l: Link) => l.rel === 'state')!
|
||||
let maxErrorCount = 5
|
||||
let errorCount = 0
|
||||
let state = currentState
|
||||
let printedState = ''
|
||||
let printedState = JobState.NoState
|
||||
let startLogLine = 0
|
||||
|
||||
const logger = process.logger || console
|
||||
|
||||
if (pollOptions) {
|
||||
pollInterval = pollOptions.pollInterval || pollInterval
|
||||
maxPollCount = pollOptions.maxPollCount || maxPollCount
|
||||
}
|
||||
|
||||
const stateLink = postedJob.links.find((l: Link) => l.rel === 'state')
|
||||
if (!stateLink) {
|
||||
throw new Error(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
while (needsRetry(state) && pollCount <= maxPollCount) {
|
||||
state = await getJobState(
|
||||
requestClient,
|
||||
@@ -194,21 +237,24 @@ const doPoll = async (
|
||||
authConfig
|
||||
).catch((err) => {
|
||||
errorCount++
|
||||
|
||||
if (pollCount >= maxPollCount || errorCount >= maxErrorCount) {
|
||||
throw err
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Error fetching job state from ${stateLink.href}. Resuming poll, assuming job to be running.`,
|
||||
err
|
||||
)
|
||||
return 'unavailable'
|
||||
|
||||
return JobState.Unavailable
|
||||
})
|
||||
|
||||
pollCount++
|
||||
|
||||
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
|
||||
|
||||
if (pollOptions?.streamLog) {
|
||||
if (streamLog) {
|
||||
const { result: job } = await requestClient.get<Job>(
|
||||
jobHref,
|
||||
authConfig?.access_token
|
||||
@@ -238,12 +284,45 @@ const doPoll = async (
|
||||
printedState = state
|
||||
}
|
||||
|
||||
if (state != 'unavailable' && errorCount > 0) {
|
||||
if (state !== JobState.Unavailable && errorCount > 0) {
|
||||
errorCount = 0
|
||||
}
|
||||
|
||||
await delay(pollInterval)
|
||||
if (state !== JobState.Completed) {
|
||||
await delay(pollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
return { state, pollCount }
|
||||
}
|
||||
|
||||
const validatePollStrategies = (strategy: PollStrategy) => {
|
||||
const throwError = (message?: string, pollOptions?: PollOptions) => {
|
||||
throw new Error(
|
||||
`Poll strategies are not valid.${message ? ` ${message}` : ''}${
|
||||
pollOptions
|
||||
? ` Invalid poll strategy: \n${JSON.stringify(pollOptions, null, 2)}`
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
strategy.forEach((pollOptions: PollOptions, i: number) => {
|
||||
const { maxPollCount, pollInterval } = pollOptions
|
||||
|
||||
if (maxPollCount < 1) {
|
||||
throwError(`'maxPollCount' has to be greater than 0.`, pollOptions)
|
||||
} else if (i !== 0) {
|
||||
const previousPollOptions = strategy[i - 1]
|
||||
|
||||
if (maxPollCount <= previousPollOptions.maxPollCount) {
|
||||
throwError(
|
||||
`'maxPollCount' has to be greater than 'maxPollCount' in previous poll strategy.`,
|
||||
pollOptions
|
||||
)
|
||||
}
|
||||
} else if (pollInterval < 1) {
|
||||
throwError(`'pollInterval' has to be greater than 0.`, pollOptions)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,14 +9,13 @@ import * as formatDataModule from '../../../utils/formatDataForRequest'
|
||||
import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
||||
import { PollOptions } from '../../../types'
|
||||
import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors'
|
||||
import { Logger, LogLevel } from '@sasjs/utils'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
|
||||
const sessionManager = new (<jest.Mock<SessionManager>>SessionManager)()
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
const defaultPollOptions: PollOptions = {
|
||||
maxPollCount: 100,
|
||||
pollInterval: 500,
|
||||
streamLog: false
|
||||
pollInterval: 500
|
||||
}
|
||||
|
||||
describe('executeScript', () => {
|
||||
@@ -452,7 +451,9 @@ describe('executeScript', () => {
|
||||
it('should throw a ComputeJobExecutionError if the job has failed', async () => {
|
||||
jest
|
||||
.spyOn(pollJobStateModule, 'pollJobState')
|
||||
.mockImplementation(() => Promise.resolve('failed'))
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(pollJobStateModule.JobState.Failed)
|
||||
)
|
||||
|
||||
const error: ComputeJobExecutionError = await executeScript(
|
||||
requestClient,
|
||||
@@ -485,7 +486,9 @@ describe('executeScript', () => {
|
||||
it('should throw a ComputeJobExecutionError if the job has errored out', async () => {
|
||||
jest
|
||||
.spyOn(pollJobStateModule, 'pollJobState')
|
||||
.mockImplementation(() => Promise.resolve('error'))
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(pollJobStateModule.JobState.Error)
|
||||
)
|
||||
|
||||
const error: ComputeJobExecutionError = await executeScript(
|
||||
requestClient,
|
||||
@@ -654,7 +657,9 @@ const setupMocks = () => {
|
||||
.mockImplementation(() => Promise.resolve(mockAuthConfig))
|
||||
jest
|
||||
.spyOn(pollJobStateModule, 'pollJobState')
|
||||
.mockImplementation(() => Promise.resolve('completed'))
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(pollJobStateModule.JobState.Completed)
|
||||
)
|
||||
jest
|
||||
.spyOn(sessionManager, 'getVariable')
|
||||
.mockImplementation(() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Logger, LogLevel } from '@sasjs/utils'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { RequestClient } from '../../../request/RequestClient'
|
||||
import { mockAuthConfig, mockJob } from './mockResponses'
|
||||
import { pollJobState } from '../pollJobState'
|
||||
@@ -6,17 +6,18 @@ import * as getTokensModule from '../../../auth/getTokens'
|
||||
import * as saveLogModule from '../saveLog'
|
||||
import * as getFileStreamModule from '../getFileStream'
|
||||
import * as isNodeModule from '../../../utils/isNode'
|
||||
import { PollOptions } from '../../../types'
|
||||
import * as delayModule from '../../../utils/delay'
|
||||
import { PollOptions, PollStrategy } from '../../../types'
|
||||
import { WriteStream } from 'fs'
|
||||
|
||||
const baseUrl = 'http://localhost'
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
requestClient['httpClient'].defaults.baseURL = baseUrl
|
||||
|
||||
const defaultPollOptions: PollOptions = {
|
||||
const defaultStreamLog = false
|
||||
const defaultPollStrategy: PollOptions = {
|
||||
maxPollCount: 100,
|
||||
pollInterval: 500,
|
||||
streamLog: false
|
||||
pollInterval: 500
|
||||
}
|
||||
|
||||
describe('pollJobState', () => {
|
||||
@@ -26,13 +27,10 @@ describe('pollJobState', () => {
|
||||
})
|
||||
|
||||
it('should get valid tokens if the authConfig has been provided', async () => {
|
||||
await pollJobState(
|
||||
requestClient,
|
||||
mockJob,
|
||||
false,
|
||||
mockAuthConfig,
|
||||
defaultPollOptions
|
||||
)
|
||||
await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
|
||||
...defaultPollStrategy,
|
||||
streamLog: defaultStreamLog
|
||||
})
|
||||
|
||||
expect(getTokensModule.getTokens).toHaveBeenCalledWith(
|
||||
requestClient,
|
||||
@@ -46,7 +44,7 @@ describe('pollJobState', () => {
|
||||
mockJob,
|
||||
false,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
)
|
||||
|
||||
expect(getTokensModule.getTokens).not.toHaveBeenCalled()
|
||||
@@ -58,7 +56,7 @@ describe('pollJobState', () => {
|
||||
{ ...mockJob, links: mockJob.links.filter((l) => l.rel !== 'state') },
|
||||
false,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
).catch((e: any) => e)
|
||||
|
||||
expect((error as Error).message).toContain('Job state link was not found.')
|
||||
@@ -72,7 +70,7 @@ describe('pollJobState', () => {
|
||||
mockJob,
|
||||
false,
|
||||
mockAuthConfig,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
)
|
||||
|
||||
expect(getTokensModule.getTokens).toHaveBeenCalledTimes(3)
|
||||
@@ -83,7 +81,7 @@ describe('pollJobState', () => {
|
||||
const { saveLog } = require('../saveLog')
|
||||
|
||||
await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
|
||||
...defaultPollOptions,
|
||||
...defaultPollStrategy,
|
||||
streamLog: true
|
||||
})
|
||||
|
||||
@@ -96,7 +94,7 @@ describe('pollJobState', () => {
|
||||
const { saveLog } = require('../saveLog')
|
||||
|
||||
await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
|
||||
...defaultPollOptions,
|
||||
...defaultPollStrategy,
|
||||
streamLog: true
|
||||
})
|
||||
|
||||
@@ -111,7 +109,7 @@ describe('pollJobState', () => {
|
||||
const { getFileStream } = require('../getFileStream')
|
||||
|
||||
await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
|
||||
...defaultPollOptions,
|
||||
...defaultPollStrategy,
|
||||
streamLog: true
|
||||
})
|
||||
|
||||
@@ -127,7 +125,7 @@ describe('pollJobState', () => {
|
||||
mockJob,
|
||||
false,
|
||||
mockAuthConfig,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
)
|
||||
|
||||
expect(saveLogModule.saveLog).not.toHaveBeenCalled()
|
||||
@@ -136,15 +134,18 @@ describe('pollJobState', () => {
|
||||
it('should return the current status when the max poll count is reached', async () => {
|
||||
mockRunningPoll()
|
||||
|
||||
const pollOptions: PollOptions = {
|
||||
...defaultPollStrategy,
|
||||
maxPollCount: 1,
|
||||
pollStrategy: []
|
||||
}
|
||||
|
||||
const state = await pollJobState(
|
||||
requestClient,
|
||||
mockJob,
|
||||
false,
|
||||
mockAuthConfig,
|
||||
{
|
||||
...defaultPollOptions,
|
||||
maxPollCount: 1
|
||||
}
|
||||
pollOptions
|
||||
)
|
||||
|
||||
expect(state).toEqual('running')
|
||||
@@ -159,7 +160,7 @@ describe('pollJobState', () => {
|
||||
false,
|
||||
mockAuthConfig,
|
||||
{
|
||||
...defaultPollOptions,
|
||||
...defaultPollStrategy,
|
||||
maxPollCount: 200,
|
||||
pollInterval: 10
|
||||
}
|
||||
@@ -176,7 +177,7 @@ describe('pollJobState', () => {
|
||||
mockJob,
|
||||
false,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
)
|
||||
|
||||
expect(requestClient.get).toHaveBeenCalledTimes(2)
|
||||
@@ -192,7 +193,7 @@ describe('pollJobState', () => {
|
||||
mockJob,
|
||||
true,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
)
|
||||
|
||||
expect((process as any).logger.info).toHaveBeenCalledTimes(4)
|
||||
@@ -222,7 +223,7 @@ describe('pollJobState', () => {
|
||||
mockJob,
|
||||
false,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
)
|
||||
|
||||
expect(requestClient.get).toHaveBeenCalledTimes(2)
|
||||
@@ -237,13 +238,119 @@ describe('pollJobState', () => {
|
||||
mockJob,
|
||||
false,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
defaultPollStrategy
|
||||
).catch((e: any) => e)
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'Error while polling job state for job j0b: Status Error'
|
||||
)
|
||||
})
|
||||
|
||||
it('should change poll strategies', async () => {
|
||||
mockSimplePoll(6)
|
||||
|
||||
const delays: number[] = []
|
||||
|
||||
jest.spyOn(delayModule, 'delay').mockImplementation((ms: number) => {
|
||||
delays.push(ms)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
const pollIntervals = [3, 4, 5, 6]
|
||||
|
||||
const pollStrategy = [
|
||||
{ maxPollCount: 2, pollInterval: pollIntervals[1] },
|
||||
{ maxPollCount: 3, pollInterval: pollIntervals[2] },
|
||||
{ maxPollCount: 4, pollInterval: pollIntervals[3] }
|
||||
]
|
||||
|
||||
const pollOptions: PollOptions = {
|
||||
maxPollCount: 1,
|
||||
pollInterval: pollIntervals[0],
|
||||
pollStrategy: pollStrategy
|
||||
}
|
||||
|
||||
await pollJobState(requestClient, mockJob, false, undefined, pollOptions)
|
||||
|
||||
expect(delays).toEqual([pollIntervals[0], ...pollIntervals])
|
||||
})
|
||||
|
||||
it('should throw an error if not valid poll strategies provided', async () => {
|
||||
// INFO: 'maxPollCount' has to be > 0
|
||||
let invalidPollStrategy = {
|
||||
maxPollCount: 0,
|
||||
pollInterval: 3
|
||||
}
|
||||
|
||||
let pollStrategy: PollStrategy = [invalidPollStrategy]
|
||||
|
||||
let expectedError = new Error(
|
||||
`Poll strategies are not valid. 'maxPollCount' has to be greater than 0. Invalid poll strategy: \n${JSON.stringify(
|
||||
invalidPollStrategy,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
|
||||
await expect(
|
||||
pollJobState(requestClient, mockJob, false, undefined, {
|
||||
...defaultPollStrategy,
|
||||
pollStrategy: pollStrategy
|
||||
})
|
||||
).rejects.toThrow(expectedError)
|
||||
|
||||
// INFO: 'maxPollCount' has to be > than 'maxPollCount' of the previous strategy
|
||||
const validPollStrategy = {
|
||||
maxPollCount: 5,
|
||||
pollInterval: 2
|
||||
}
|
||||
|
||||
invalidPollStrategy = {
|
||||
maxPollCount: validPollStrategy.maxPollCount,
|
||||
pollInterval: 3
|
||||
}
|
||||
|
||||
pollStrategy = [validPollStrategy, invalidPollStrategy]
|
||||
|
||||
expectedError = new Error(
|
||||
`Poll strategies are not valid. 'maxPollCount' has to be greater than 'maxPollCount' in previous poll strategy. Invalid poll strategy: \n${JSON.stringify(
|
||||
invalidPollStrategy,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
|
||||
await expect(
|
||||
pollJobState(requestClient, mockJob, false, undefined, {
|
||||
...defaultPollStrategy,
|
||||
pollStrategy: pollStrategy
|
||||
})
|
||||
).rejects.toThrow(expectedError)
|
||||
|
||||
// INFO: invalid 'pollInterval'
|
||||
invalidPollStrategy = {
|
||||
maxPollCount: 1,
|
||||
pollInterval: 0
|
||||
}
|
||||
|
||||
pollStrategy = [invalidPollStrategy]
|
||||
|
||||
expectedError = new Error(
|
||||
`Poll strategies are not valid. 'pollInterval' has to be greater than 0. Invalid poll strategy: \n${JSON.stringify(
|
||||
invalidPollStrategy,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
|
||||
await expect(
|
||||
pollJobState(requestClient, mockJob, false, undefined, {
|
||||
...defaultPollStrategy,
|
||||
pollStrategy: pollStrategy
|
||||
})
|
||||
).rejects.toThrow(expectedError)
|
||||
})
|
||||
})
|
||||
|
||||
const setupMocks = () => {
|
||||
@@ -273,11 +380,14 @@ const setupMocks = () => {
|
||||
|
||||
const mockSimplePoll = (runningCount = 2) => {
|
||||
let count = 0
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation((url) => {
|
||||
count++
|
||||
|
||||
if (url.includes('job')) {
|
||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
result:
|
||||
count === 0
|
||||
@@ -293,11 +403,14 @@ const mockSimplePoll = (runningCount = 2) => {
|
||||
|
||||
const mockRunningPoll = () => {
|
||||
let count = 0
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation((url) => {
|
||||
count++
|
||||
|
||||
if (url.includes('job')) {
|
||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
result: count === 0 ? 'pending' : 'running',
|
||||
etag: '',
|
||||
@@ -308,11 +421,14 @@ const mockRunningPoll = () => {
|
||||
|
||||
const mockLongPoll = () => {
|
||||
let count = 0
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation((url) => {
|
||||
count++
|
||||
|
||||
if (url.includes('job')) {
|
||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
result: count <= 102 ? 'running' : 'completed',
|
||||
etag: '',
|
||||
@@ -323,14 +439,18 @@ const mockLongPoll = () => {
|
||||
|
||||
const mockPollWithSingleError = () => {
|
||||
let count = 0
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation((url) => {
|
||||
count++
|
||||
|
||||
if (url.includes('job')) {
|
||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||
}
|
||||
|
||||
if (count === 1) {
|
||||
return Promise.reject('Status Error')
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
result: count === 0 ? 'pending' : 'completed',
|
||||
etag: '',
|
||||
@@ -344,6 +464,7 @@ const mockErroredPoll = () => {
|
||||
if (url.includes('job')) {
|
||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||
}
|
||||
|
||||
return Promise.reject('Status Error')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Logger, LogLevel } from '@sasjs/utils'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { RequestClient } from '../../../request/RequestClient'
|
||||
import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
||||
import * as writeStreamModule from '../writeStream'
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
fileExists,
|
||||
readFile,
|
||||
deleteFile
|
||||
} from '@sasjs/utils'
|
||||
} from '@sasjs/utils/file'
|
||||
|
||||
describe('writeStream', () => {
|
||||
const filename = 'test.txt'
|
||||
|
||||
@@ -78,7 +78,16 @@ export class AuthManager {
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
await this.performCASSecurityCheck()
|
||||
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||
|
||||
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||
return {
|
||||
isLoggedIn: false,
|
||||
userName: this.userName || '',
|
||||
userLongName: this.userLongName || '',
|
||||
errorMessage: 'Public access has been denied.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { userName, userLongName } = await this.fetchUserName()
|
||||
@@ -149,7 +158,17 @@ export class AuthManager {
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
await this.performCASSecurityCheck()
|
||||
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||
isLoggedIn = false
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName || '',
|
||||
userLongName: this.userLongName || '',
|
||||
errorMessage: 'Public access has been denied.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loginCallback()
|
||||
@@ -166,11 +185,15 @@ export class AuthManager {
|
||||
private async performCASSecurityCheck() {
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient
|
||||
return await this.requestClient
|
||||
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
||||
.catch((err) => {
|
||||
// ignore if resource not found error
|
||||
if (!(err instanceof NotFoundError)) throw err
|
||||
|
||||
return {
|
||||
result: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -387,3 +410,7 @@ const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
|
||||
|
||||
return /You have signed in/gm.test(response)
|
||||
}
|
||||
|
||||
const isPublicAccessDenied = (response: any): boolean => {
|
||||
return /Public access has been denied/gm.test(response)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
|
||||
import { ServerType } from '@sasjs/utils'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
|
||||
/**
|
||||
* Exchanges the auth code for an access token for the given client.
|
||||
|
||||
@@ -4,7 +4,6 @@ import { RequestClient } from '../request/RequestClient'
|
||||
import { CertificateError } from '../types/errors'
|
||||
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
|
||||
|
||||
// TODO: update func docs
|
||||
/**
|
||||
* Exchange the auth code for access / refresh tokens for the given client / secret pair.
|
||||
* @param requestClient - the pre-configured HTTP request client.
|
||||
@@ -31,10 +30,11 @@ export async function getAccessTokenForViya(
|
||||
Authorization: 'Basic ' + token,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
const dataJson = {
|
||||
|
||||
const dataJson = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: authCode
|
||||
}
|
||||
})
|
||||
const data = new URLSearchParams(dataJson)
|
||||
|
||||
const authResponse = await requestClient
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
|
||||
import { ServerType } from '@sasjs/utils'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
|
||||
/**
|
||||
* Exchanges the refresh token for an access token for the given client.
|
||||
|
||||
@@ -5,6 +5,7 @@ import axios from 'axios'
|
||||
import {
|
||||
mockedCurrentUserApi,
|
||||
mockLoginAuthoriseRequiredResponse,
|
||||
mockLoginPublicAccessDeniedResponse,
|
||||
mockLoginSuccessResponse
|
||||
} from './mockResponses'
|
||||
import { serialize } from '../../utils'
|
||||
@@ -213,6 +214,61 @@ describe('AuthManager', () => {
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should post a login & a cas_security request to the SAS9 server when not logged in & get rejected due to public access denied', async () => {
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
userLongName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
mockedAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
expect(loginResponse.errorMessage).toEqual(
|
||||
'Public access has been denied.'
|
||||
)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
const casAuthenticationUrl = `${serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
getHeadersJson
|
||||
)
|
||||
})
|
||||
|
||||
it('should return empty username if unable to logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
@@ -422,6 +478,53 @@ describe('AuthManager', () => {
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return error if public account access is denied', async () => {
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: ''
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName
|
||||
})
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.redirectedLogIn({})
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
expect(loginResponse.errorMessage).toEqual(
|
||||
'Public access has been denied.'
|
||||
)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
|
||||
expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return empty username if user unable to re-login via pop up', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthConfig } from '@sasjs/utils'
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
import { generateToken, mockSasjsAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { getAccessTokenForSasjs } from '../getAccessTokenForSasjs'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthConfig } from '@sasjs/utils'
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthConfig } from '@sasjs/utils'
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
import * as refreshTokensModule from '../refreshTokensForViya'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { getTokens } from '../getTokens'
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 mockLoginSuccessResponse = `You have signed in`
|
||||
export const mockLoginPublicAccessDeniedResponse = `Public access has been denied`
|
||||
|
||||
export const mockAuthResponse: SasAuthResponse = {
|
||||
access_token: 'acc355',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServerType } from '@sasjs/utils'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { refreshTokensForSasjs } from '../refreshTokensForSasjs'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils'
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { convertToCSV } from '../utils/convertToCsv'
|
||||
import { isNode } from '../utils'
|
||||
|
||||
/**
|
||||
* One of the approaches SASjs takes to send tables-formatted JSON (see README)
|
||||
@@ -26,12 +27,15 @@ export const generateFileUploadForm = (
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof FormData === 'undefined' && formData instanceof NodeFormData) {
|
||||
formData.append(name, csv, {
|
||||
// INFO: unfortunately it is not possible to check if formData is instance of NodeFormData or FormData because it will return true for both
|
||||
if (isNode()) {
|
||||
// INFO: environment is Node and formData is instance of NodeFormData
|
||||
;(formData as NodeFormData).append(name, csv, {
|
||||
filename: `${name}.csv`,
|
||||
contentType: 'application/csv'
|
||||
})
|
||||
} else {
|
||||
// INFO: environment is Browser and formData is instance of FormData
|
||||
const file = new Blob([csv], {
|
||||
type: 'application/csv'
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { generateFileUploadForm } from '../generateFileUploadForm'
|
||||
import { convertToCSV } from '../../utils/convertToCsv'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import * as isNodeModule from '../../utils/isNode'
|
||||
|
||||
describe('generateFileUploadForm', () => {
|
||||
beforeAll(() => {
|
||||
@@ -11,44 +14,94 @@ describe('generateFileUploadForm', () => {
|
||||
;(global as any).Blob = BlobMock
|
||||
})
|
||||
|
||||
it('should generate file upload form from data', () => {
|
||||
const formData = new FormData()
|
||||
const testTable = 'sometable'
|
||||
const testTableWithNullVars: { [key: string]: any } = {
|
||||
[testTable]: [
|
||||
{ var1: 'string', var2: 232, nullvar: 'A' },
|
||||
{ var1: 'string', var2: 232, nullvar: 'B' },
|
||||
{ var1: 'string', var2: 232, nullvar: '_' },
|
||||
{ var1: 'string', var2: 232, nullvar: 0 },
|
||||
{ var1: 'string', var2: 232, nullvar: 'z' },
|
||||
{ var1: 'string', var2: 232, nullvar: null }
|
||||
],
|
||||
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
|
||||
}
|
||||
const tableName = Object.keys(testTableWithNullVars).filter((key: string) =>
|
||||
Array.isArray(testTableWithNullVars[key])
|
||||
)[0]
|
||||
describe('browser', () => {
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
jest.spyOn(formData, 'append').mockImplementation(() => {})
|
||||
it('should generate file upload form from data', () => {
|
||||
const formData = new FormData()
|
||||
const testTable = 'sometable'
|
||||
const testTableWithNullVars: { [key: string]: any } = {
|
||||
[testTable]: [
|
||||
{ var1: 'string', var2: 232, nullvar: 'A' },
|
||||
{ var1: 'string', var2: 232, nullvar: 'B' },
|
||||
{ var1: 'string', var2: 232, nullvar: '_' },
|
||||
{ var1: 'string', var2: 232, nullvar: 0 },
|
||||
{ var1: 'string', var2: 232, nullvar: 'z' },
|
||||
{ var1: 'string', var2: 232, nullvar: null }
|
||||
],
|
||||
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
|
||||
}
|
||||
const tableName = Object.keys(testTableWithNullVars).filter(
|
||||
(key: string) => Array.isArray(testTableWithNullVars[key])
|
||||
)[0]
|
||||
|
||||
generateFileUploadForm(formData, testTableWithNullVars)
|
||||
jest.spyOn(formData, 'append').mockImplementation(() => {})
|
||||
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => false)
|
||||
|
||||
expect(formData.append).toHaveBeenCalledOnce()
|
||||
expect(formData.append).toHaveBeenCalledWith(
|
||||
tableName,
|
||||
{},
|
||||
`${tableName}.csv`
|
||||
)
|
||||
generateFileUploadForm(formData, testTableWithNullVars)
|
||||
|
||||
expect(formData.append).toHaveBeenCalledOnce()
|
||||
expect(formData.append).toHaveBeenCalledWith(
|
||||
tableName,
|
||||
{},
|
||||
`${tableName}.csv`
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if too large string was provided', () => {
|
||||
const formData = new FormData()
|
||||
const data = { testTable: [{ var1: 'z'.repeat(32765 + 1) }] }
|
||||
|
||||
expect(() => generateFileUploadForm(formData, data)).toThrow(
|
||||
new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if too large string was provided', () => {
|
||||
const formData = new FormData()
|
||||
const data = { testTable: [{ var1: 'z'.repeat(32765 + 1) }] }
|
||||
describe('node', () => {
|
||||
it('should generate file upload form from data', () => {
|
||||
const formData = new NodeFormData()
|
||||
const testTable = 'sometable'
|
||||
const testTableWithNullVars: { [key: string]: any } = {
|
||||
[testTable]: [
|
||||
{ var1: 'string', var2: 232, nullvar: 'A' },
|
||||
{ var1: 'string', var2: 232, nullvar: 'B' },
|
||||
{ var1: 'string', var2: 232, nullvar: '_' },
|
||||
{ var1: 'string', var2: 232, nullvar: 0 },
|
||||
{ var1: 'string', var2: 232, nullvar: 'z' },
|
||||
{ var1: 'string', var2: 232, nullvar: null }
|
||||
],
|
||||
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
|
||||
}
|
||||
const tableName = Object.keys(testTableWithNullVars).filter(
|
||||
(key: string) => Array.isArray(testTableWithNullVars[key])
|
||||
)[0]
|
||||
const csv = convertToCSV(testTableWithNullVars, tableName)
|
||||
|
||||
expect(() => generateFileUploadForm(formData, data)).toThrow(
|
||||
new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
jest.spyOn(formData, 'append').mockImplementation(() => {})
|
||||
|
||||
generateFileUploadForm(formData, testTableWithNullVars)
|
||||
|
||||
expect(formData.append).toHaveBeenCalledOnce()
|
||||
expect(formData.append).toHaveBeenCalledWith(tableName, csv, {
|
||||
contentType: 'application/csv',
|
||||
filename: `${tableName}.csv`
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if too large string was provided', () => {
|
||||
const formData = new NodeFormData()
|
||||
const data = { testTable: [{ var1: 'z'.repeat(32765 + 1) }] }
|
||||
|
||||
expect(() => generateFileUploadForm(formData, data)).toThrow(
|
||||
new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,15 +93,24 @@ export class FileUploader extends BaseJobExecutor {
|
||||
this.requestClient,
|
||||
config.serverUrl
|
||||
)
|
||||
|
||||
break
|
||||
case ServerType.Sas9:
|
||||
jsonResponse =
|
||||
typeof res.result === 'string'
|
||||
? parseWeboutResponse(res.result, uploadUrl)
|
||||
: res.result
|
||||
|
||||
break
|
||||
case ServerType.Sasjs:
|
||||
jsonResponse =
|
||||
typeof res.result === 'string'
|
||||
? getValidJson(res.result)
|
||||
: res.result
|
||||
|
||||
break
|
||||
}
|
||||
} else if (this.serverType !== ServerType.Sasjs) {
|
||||
} else {
|
||||
jsonResponse =
|
||||
typeof res.result === 'string'
|
||||
? getValidJson(res.result)
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
LoginRequiredError
|
||||
} from '../types/errors'
|
||||
import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
||||
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { getFormData } from '../utils'
|
||||
|
||||
import {
|
||||
isRelativePath,
|
||||
@@ -53,8 +53,7 @@ export class SasjsJobExecutor extends BaseJobExecutor {
|
||||
* Use the available form data object (FormData in Browser, NodeFormData in
|
||||
* Node)
|
||||
*/
|
||||
let formData =
|
||||
typeof FormData === 'undefined' ? new NodeFormData() : new FormData()
|
||||
let formData = getFormData()
|
||||
|
||||
if (data) {
|
||||
// file upload approach
|
||||
@@ -93,8 +92,10 @@ export class SasjsJobExecutor extends BaseJobExecutor {
|
||||
)
|
||||
}
|
||||
|
||||
const { result } = res.result
|
||||
if (result && result.trim()) res.result = getValidJson(result)
|
||||
const { result } = res
|
||||
|
||||
if (result && typeof result === 'string' && result.trim())
|
||||
res.result = getValidJson(result)
|
||||
|
||||
this.requestClient!.appendRequest(res, sasJob, config.debug)
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import {
|
||||
isRelativePath,
|
||||
parseSasViyaDebugResponse,
|
||||
appendExtraResponseAttributes
|
||||
appendExtraResponseAttributes,
|
||||
parseWeboutResponse,
|
||||
getFormData
|
||||
} from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
|
||||
export interface WaitingRequstPromise {
|
||||
promise: Promise<any> | null
|
||||
@@ -112,8 +113,7 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
* Use the available form data object (FormData in Browser, NodeFormData in
|
||||
* Node)
|
||||
*/
|
||||
let formData =
|
||||
typeof FormData === 'undefined' ? new NodeFormData() : new FormData()
|
||||
let formData = getFormData()
|
||||
|
||||
if (data) {
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
@@ -187,6 +187,12 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
{ result: jsonResponse, log: res.log },
|
||||
extraResponseAttributes
|
||||
)
|
||||
|
||||
if (this.isPublicAccessDenied(jsonResponse))
|
||||
reject(
|
||||
new ErrorResponse('Public access has been denied', responseObject)
|
||||
)
|
||||
|
||||
resolve(responseObject)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
@@ -262,4 +268,8 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
private isPublicAccessDenied = (response: string): boolean => {
|
||||
return /Public access has been denied/gm.test(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as pem from 'pem'
|
||||
import * as http from 'http'
|
||||
import * as https from 'https'
|
||||
import { app, mockedAuthResponse } from './SAS_server_app'
|
||||
import { ServerType } from '@sasjs/utils'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import SASjs from '../SASjs'
|
||||
import * as axiosModules from '../utils/createAxiosInstance'
|
||||
import {
|
||||
@@ -213,7 +213,7 @@ describe('RequestClient - Self Signed Server', () => {
|
||||
serverType: ServerType.SasViya
|
||||
})
|
||||
|
||||
const expectedError = 'self signed certificate'
|
||||
const expectedError = 'self-signed certificate'
|
||||
|
||||
const rejectionErrorMessage = await adapterWithoutCertificate
|
||||
.getAccessToken('clientId', 'clientSecret', 'authCode')
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SessionManager } from '../SessionManager'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import * as dotenv from 'dotenv'
|
||||
import axios from 'axios'
|
||||
import { Logger, LogLevel } from '@sasjs/utils'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { Session, Context } from '../types'
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface LoginResult {
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
userLongName: string
|
||||
errorMessage?: string
|
||||
}
|
||||
export interface LoginResultInternal {
|
||||
isLoggedIn: boolean
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export interface PollOptions {
|
||||
maxPollCount: number
|
||||
pollInterval: number
|
||||
streamLog: boolean
|
||||
pollInterval: number // milliseconds
|
||||
pollStrategy?: PollStrategy
|
||||
streamLog?: boolean
|
||||
logFolderPath?: string
|
||||
}
|
||||
|
||||
export type PollStrategy = PollOptions[]
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('RootFolderNotFoundError', () => {
|
||||
|
||||
const error = new RootFolderNotFoundError(
|
||||
'/myProject',
|
||||
'https://analytium.co.uk',
|
||||
'https://sas.4gl.io',
|
||||
token
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('RootFolderNotFoundError', () => {
|
||||
it('when access token is not provided, error message should not contain scopes', () => {
|
||||
const error = new RootFolderNotFoundError(
|
||||
'/myProject',
|
||||
'https://analytium.co.uk'
|
||||
'https://sas.4gl.io'
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(RootFolderNotFoundError)
|
||||
@@ -30,7 +30,7 @@ describe('RootFolderNotFoundError', () => {
|
||||
|
||||
it('should include the folder path and SASDrive URL in the message', () => {
|
||||
const folderPath = '/myProject'
|
||||
const serverUrl = 'https://analytium.co.uk'
|
||||
const serverUrl = 'https://sas.4gl.io'
|
||||
const error = new RootFolderNotFoundError(folderPath, serverUrl)
|
||||
|
||||
expect(error).toBeInstanceOf(RootFolderNotFoundError)
|
||||
|
||||
5
src/utils/getFormData.ts
Normal file
5
src/utils/getFormData.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { isNode } from './'
|
||||
import * as NodeFormData from 'form-data'
|
||||
|
||||
export const getFormData = () =>
|
||||
isNode() ? new NodeFormData() : new FormData()
|
||||
@@ -20,3 +20,4 @@ export * from './parseWeboutResponse'
|
||||
export * from './serialize'
|
||||
export * from './splitChunks'
|
||||
export * from './validateInput'
|
||||
export * from './getFormData'
|
||||
|
||||
20
src/utils/spec/getFormData.spec.ts
Normal file
20
src/utils/spec/getFormData.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { getFormData } from '..'
|
||||
import * as isNodeModule from '../isNode'
|
||||
import * as NodeFormData from 'form-data'
|
||||
|
||||
describe('getFormData', () => {
|
||||
it('should return NodeFormData if environment is Node', () => {
|
||||
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => true)
|
||||
|
||||
expect(getFormData() instanceof NodeFormData).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return FormData if environment is not Node', () => {
|
||||
const formDataMock = () => {}
|
||||
;(global as any).FormData = formDataMock
|
||||
|
||||
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => false)
|
||||
|
||||
expect(getFormData() instanceof FormData).toEqual(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user