mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-10 22:00:05 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4c4511913c | ||
|
|
8c64c24f3c | ||
|
|
1f2f445002 | ||
|
|
6afa056a86 | ||
|
|
fe47ca1152 | ||
|
|
10da691f0f | ||
|
|
318f9694cb | ||
|
|
56e6131e5c | ||
| 5dfee30875 | |||
|
|
3a186bc55c | ||
|
|
0359fcb6be | ||
|
|
f2997169cb | ||
|
|
451f2dfaca |
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
|
||||||
|
|||||||
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -22,12 +22,16 @@ jobs:
|
|||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Check npm audit
|
# FIXME: uncomment 'Check npm audit' step after axios version bump
|
||||||
run: npm audit --production --audit-level=low
|
# - name: Check npm audit
|
||||||
|
# run: npm audit --production --audit-level=low
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Rimraf
|
||||||
|
run: npm i rimraf
|
||||||
|
|
||||||
- name: Check code style
|
- name: Check code style
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
@@ -52,6 +56,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
|
||||||
@@ -67,6 +75,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
|
||||||
@@ -75,8 +86,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
|
||||||
|
|
||||||
@@ -89,6 +102,9 @@ 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
|
||||||
|
cat ./cypress.json
|
||||||
|
echo "SASJS_USERNAME=${{ secrets.SASJS_USERNAME }}"
|
||||||
|
|
||||||
sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
|
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
|
# 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
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": ["SASVIYA"]
|
||||||
|
}
|
||||||
@@ -43,10 +43,10 @@ module.exports = {
|
|||||||
// An object that configures minimum threshold enforcement for coverage results
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
statements: 63.66,
|
statements: 64.03,
|
||||||
branches: 44.74,
|
branches: 45.11,
|
||||||
functions: 53.94,
|
functions: 54.18,
|
||||||
lines: 64.12
|
lines: 64.53
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
20353
package-lock.json
generated
20353
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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}\"",
|
||||||
@@ -62,7 +62,6 @@
|
|||||||
"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": "27.1.3",
|
||||||
@@ -77,7 +76,7 @@
|
|||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "2.52.0",
|
"@sasjs/utils": "^3.5.1",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"axios-cookiejar-support": "1.0.1",
|
"axios-cookiejar-support": "1.0.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
|
|||||||
14155
sasjs-tests/package-lock.json
generated
14155
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ 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 we can't process the special characters
|
||||||
|
if (res.SYSENCODING === 'latin9') 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 &&
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { prefixMessage } from '@sasjs/utils/error'
|
|||||||
import { pollJobState } from './api/viya/pollJobState'
|
import { pollJobState } from './api/viya/pollJobState'
|
||||||
import { getTokens } from './auth/getTokens'
|
import { getTokens } from './auth/getTokens'
|
||||||
import { uploadTables } from './api/viya/uploadTables'
|
import { uploadTables } from './api/viya/uploadTables'
|
||||||
import { executeScript } from './api/viya/executeScript'
|
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'
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ export class SASViyaApiClient {
|
|||||||
printPid = false,
|
printPid = false,
|
||||||
variables?: MacroVar
|
variables?: MacroVar
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return executeScript(
|
return executeOnComputeApi(
|
||||||
this.requestClient,
|
this.requestClient,
|
||||||
this.sessionManager,
|
this.sessionManager,
|
||||||
this.rootFolderName,
|
this.rootFolderName,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Session, Context, SessionVariable } from './types'
|
import { Session, Context, SessionVariable, SessionState } from './types'
|
||||||
import { NoSessionStateError } from './types/errors'
|
import { NoSessionStateError } from './types/errors'
|
||||||
import { asyncForEach, isUrl } from './utils'
|
import { asyncForEach, isUrl } from './utils'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
@@ -12,6 +12,7 @@ interface ApiErrorResponse {
|
|||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
private loggedErrors: NoSessionStateError[] = []
|
private loggedErrors: NoSessionStateError[] = []
|
||||||
|
private sessionStateLinkError = 'Error while getting session state link. '
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
@@ -28,7 +29,7 @@ export class SessionManager {
|
|||||||
private _debug: boolean = false
|
private _debug: boolean = false
|
||||||
private printedSessionState = {
|
private printedSessionState = {
|
||||||
printed: false,
|
printed: false,
|
||||||
state: ''
|
state: SessionState.NoState
|
||||||
}
|
}
|
||||||
|
|
||||||
public get debug() {
|
public get debug() {
|
||||||
@@ -265,6 +266,18 @@ export class SessionManager {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add response etag to Session object.
|
||||||
|
createdSession.etag = etag
|
||||||
|
|
||||||
|
// Get session state link.
|
||||||
|
const stateLink = createdSession.links.find((link) => link.rel === 'state')
|
||||||
|
|
||||||
|
// Throw error if session state link is not present.
|
||||||
|
if (!stateLink) throw this.sessionStateLinkError
|
||||||
|
|
||||||
|
// Add session state link to Session object.
|
||||||
|
createdSession.stateUrl = stateLink.href
|
||||||
|
|
||||||
await this.waitForSession(createdSession, etag, accessToken)
|
await this.waitForSession(createdSession, etag, accessToken)
|
||||||
|
|
||||||
this.sessions.push(createdSession)
|
this.sessions.push(createdSession)
|
||||||
@@ -327,32 +340,30 @@ export class SessionManager {
|
|||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
let { state: sessionState } = session
|
||||||
|
const { stateUrl } = session
|
||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
|
|
||||||
let sessionState = session.state
|
|
||||||
|
|
||||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
sessionState === 'pending' ||
|
sessionState === SessionState.Pending ||
|
||||||
sessionState === 'running' ||
|
sessionState === SessionState.Running ||
|
||||||
sessionState === ''
|
sessionState === SessionState.NoState
|
||||||
) {
|
) {
|
||||||
if (stateLink) {
|
if (stateUrl) {
|
||||||
if (this.debug && !this.printedSessionState.printed) {
|
if (this.debug && !this.printedSessionState.printed) {
|
||||||
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
|
logger.info(`Polling: ${this.serverUrl + stateUrl}`)
|
||||||
|
|
||||||
this.printedSessionState.printed = true
|
this.printedSessionState.printed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this.serverUrl}${stateLink.href}?wait=30`
|
const url = `${this.serverUrl}${stateUrl}?wait=30`
|
||||||
|
|
||||||
const { result: state, responseStatus: responseStatus } =
|
const { result: state, responseStatus: responseStatus } =
|
||||||
await this.getSessionState(url, etag!, accessToken).catch((err) => {
|
await this.getSessionState(url, etag!, accessToken).catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while waiting for session. ')
|
throw prefixMessage(err, 'Error while waiting for session. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
sessionState = state.trim()
|
sessionState = state.trim() as SessionState
|
||||||
|
|
||||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||||
logger.info(`Current session state is '${sessionState}'`)
|
logger.info(`Current session state is '${sessionState}'`)
|
||||||
@@ -364,7 +375,7 @@ export class SessionManager {
|
|||||||
if (!sessionState) {
|
if (!sessionState) {
|
||||||
const stateError = new NoSessionStateError(
|
const stateError = new NoSessionStateError(
|
||||||
responseStatus,
|
responseStatus,
|
||||||
this.serverUrl + stateLink.href,
|
this.serverUrl + stateUrl,
|
||||||
session.links.find((l: any) => l.rel === 'log')?.href as string
|
session.links.find((l: any) => l.rel === 'log')?.href as string
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -386,7 +397,7 @@ export class SessionManager {
|
|||||||
|
|
||||||
return sessionState
|
return sessionState
|
||||||
} else {
|
} else {
|
||||||
throw 'Error while getting session state link. '
|
throw this.sessionStateLinkError
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.loggedErrors = []
|
this.loggedErrors = []
|
||||||
@@ -413,7 +424,7 @@ export class SessionManager {
|
|||||||
return await this.requestClient
|
return await this.requestClient
|
||||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||||
.then((res) => ({
|
.then((res) => ({
|
||||||
result: res.result as string,
|
result: res.result as SessionState,
|
||||||
responseStatus: res.status
|
responseStatus: res.status
|
||||||
}))
|
}))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ import { formatDataForRequest } from '../../utils/formatDataForRequest'
|
|||||||
import { pollJobState, JobState } from './pollJobState'
|
import { pollJobState, JobState } from './pollJobState'
|
||||||
import { uploadTables } from './uploadTables'
|
import { uploadTables } from './uploadTables'
|
||||||
|
|
||||||
|
interface JobRequestBody {
|
||||||
|
[key: string]: number | string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes code on the current SAS Viya server.
|
* Executes SAS program on the current SAS Viya server using Compute API.
|
||||||
* @param jobPath - the path to the file being submitted for execution.
|
* @param jobPath - the path to the file being submitted for execution.
|
||||||
* @param linesOfCode - an array of code lines to execute.
|
* @param linesOfCode - an array of code lines to execute.
|
||||||
* @param contextName - the context to execute the code in.
|
* @param contextName - the context to execute the code in.
|
||||||
@@ -29,7 +33,7 @@ import { uploadTables } from './uploadTables'
|
|||||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||||
* @param variables - an object that represents macro variables.
|
* @param variables - an object that represents macro variables.
|
||||||
*/
|
*/
|
||||||
export async function executeScript(
|
export async function executeOnComputeApi(
|
||||||
requestClient: RequestClient,
|
requestClient: RequestClient,
|
||||||
sessionManager: SessionManager,
|
sessionManager: SessionManager,
|
||||||
rootFolderName: string,
|
rootFolderName: string,
|
||||||
@@ -46,6 +50,7 @@ export async function executeScript(
|
|||||||
variables?: MacroVar
|
variables?: MacroVar
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
let access_token = (authConfig || {}).access_token
|
let access_token = (authConfig || {}).access_token
|
||||||
|
|
||||||
if (authConfig) {
|
if (authConfig) {
|
||||||
;({ access_token } = await getTokens(requestClient, authConfig))
|
;({ access_token } = await getTokens(requestClient, authConfig))
|
||||||
}
|
}
|
||||||
@@ -85,20 +90,6 @@ export async function executeScript(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobArguments: { [key: string]: any } = {
|
|
||||||
_contextName: contextName,
|
|
||||||
_OMITJSONLISTING: true,
|
|
||||||
_OMITJSONLOG: true,
|
|
||||||
_OMITSESSIONRESULTS: true,
|
|
||||||
_OMITTEXTLISTING: true,
|
|
||||||
_OMITTEXTLOG: true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
jobArguments['_OMITTEXTLOG'] = false
|
|
||||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileName
|
let fileName
|
||||||
|
|
||||||
if (isRelativePath(jobPath)) {
|
if (isRelativePath(jobPath)) {
|
||||||
@@ -107,6 +98,7 @@ export async function executeScript(
|
|||||||
}`
|
}`
|
||||||
} else {
|
} else {
|
||||||
const jobPathParts = jobPath.split('/')
|
const jobPathParts = jobPath.split('/')
|
||||||
|
|
||||||
fileName = jobPathParts.pop()
|
fileName = jobPathParts.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +110,6 @@ export async function executeScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variables) jobVariables = { ...jobVariables, ...variables }
|
if (variables) jobVariables = { ...jobVariables, ...variables }
|
||||||
|
|
||||||
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
||||||
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
@@ -145,12 +136,12 @@ export async function executeScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute job in session
|
// Execute job in session
|
||||||
const jobRequestBody = {
|
const jobRequestBody: JobRequestBody = {
|
||||||
name: fileName,
|
name: fileName || 'Default Job Name',
|
||||||
description: 'Powered by SASjs',
|
description: 'Powered by SASjs',
|
||||||
code: linesOfCode,
|
code: linesOfCode,
|
||||||
variables: jobVariables,
|
variables: jobVariables,
|
||||||
arguments: jobArguments
|
version: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: postedJob, etag } = await requestClient
|
const { result: postedJob, etag } = await requestClient
|
||||||
@@ -179,16 +170,21 @@ export async function executeScript(
|
|||||||
postedJob,
|
postedJob,
|
||||||
debug,
|
debug,
|
||||||
authConfig,
|
authConfig,
|
||||||
pollOptions
|
pollOptions,
|
||||||
|
{
|
||||||
|
session,
|
||||||
|
sessionManager
|
||||||
|
}
|
||||||
).catch(async (err) => {
|
).catch(async (err) => {
|
||||||
const error = err?.response?.data
|
const error = err?.response?.data
|
||||||
const result = /err=[0-9]*,/.exec(error)
|
const result = /err=[0-9]*,/.exec(error)
|
||||||
|
|
||||||
const errorCode = '5113'
|
const errorCode = '5113'
|
||||||
|
|
||||||
if (result?.[0]?.slice(4, -1) === errorCode) {
|
if (result?.[0]?.slice(4, -1) === errorCode) {
|
||||||
|
const logCount = 1000000
|
||||||
const sessionLogUrl =
|
const sessionLogUrl =
|
||||||
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
||||||
const logCount = 1000000
|
|
||||||
err.log = await fetchLogByChunks(
|
err.log = await fetchLogByChunks(
|
||||||
requestClient,
|
requestClient,
|
||||||
access_token!,
|
access_token!,
|
||||||
@@ -196,6 +192,7 @@ export async function executeScript(
|
|||||||
logCount
|
logCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw prefixMessage(err, 'Error while polling job status. ')
|
throw prefixMessage(err, 'Error while polling job status. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,12 +211,12 @@ export async function executeScript(
|
|||||||
|
|
||||||
let jobResult
|
let jobResult
|
||||||
let log = ''
|
let log = ''
|
||||||
|
|
||||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||||
|
|
||||||
if (debug && logLink) {
|
if (debug && logLink) {
|
||||||
const logUrl = `${logLink.href}/content`
|
const logUrl = `${logLink.href}/content`
|
||||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
|
|
||||||
log = await fetchLogByChunks(
|
log = await fetchLogByChunks(
|
||||||
requestClient,
|
requestClient,
|
||||||
access_token!,
|
access_token!,
|
||||||
@@ -232,9 +229,7 @@ export async function executeScript(
|
|||||||
throw new ComputeJobExecutionError(currentJob, log)
|
throw new ComputeJobExecutionError(currentJob, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!expectWebout) {
|
if (!expectWebout) return { job: currentJob, log }
|
||||||
return { job: currentJob, log }
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
||||||
|
|
||||||
@@ -245,6 +240,7 @@ export async function executeScript(
|
|||||||
if (logLink) {
|
if (logLink) {
|
||||||
const logUrl = `${logLink.href}/content`
|
const logUrl = `${logLink.href}/content`
|
||||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
|
|
||||||
log = await fetchLogByChunks(
|
log = await fetchLogByChunks(
|
||||||
requestClient,
|
requestClient,
|
||||||
access_token!,
|
access_token!,
|
||||||
@@ -279,7 +275,7 @@ export async function executeScript(
|
|||||||
const error = e as HttpError
|
const error = e as HttpError
|
||||||
|
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
return executeScript(
|
return executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
rootFolderName,
|
rootFolderName,
|
||||||
@@ -3,7 +3,7 @@ import { Job, PollOptions, PollStrategy } from '../..'
|
|||||||
import { getTokens } from '../../auth/getTokens'
|
import { getTokens } from '../../auth/getTokens'
|
||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import { JobStatePollError } from '../../types/errors'
|
import { JobStatePollError } from '../../types/errors'
|
||||||
import { Link, WriteStream } from '../../types'
|
import { Link, WriteStream, SessionState, JobSessionManager } from '../../types'
|
||||||
import { delay, isNode } from '../../utils'
|
import { delay, isNode } from '../../utils'
|
||||||
|
|
||||||
export enum JobState {
|
export enum JobState {
|
||||||
@@ -37,6 +37,7 @@ export enum JobState {
|
|||||||
* { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.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))
|
* { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
|
||||||
* ]
|
* ]
|
||||||
|
* @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state.
|
||||||
* @returns - a promise which resolves with a job state
|
* @returns - a promise which resolves with a job state
|
||||||
*/
|
*/
|
||||||
export async function pollJobState(
|
export async function pollJobState(
|
||||||
@@ -44,7 +45,8 @@ export async function pollJobState(
|
|||||||
postedJob: Job,
|
postedJob: Job,
|
||||||
debug: boolean,
|
debug: boolean,
|
||||||
authConfig?: AuthConfig,
|
authConfig?: AuthConfig,
|
||||||
pollOptions?: PollOptions
|
pollOptions?: PollOptions,
|
||||||
|
jobSessionManager?: JobSessionManager
|
||||||
): Promise<JobState> {
|
): Promise<JobState> {
|
||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
|
|
||||||
@@ -127,7 +129,8 @@ export async function pollJobState(
|
|||||||
pollOptions,
|
pollOptions,
|
||||||
authConfig,
|
authConfig,
|
||||||
streamLog,
|
streamLog,
|
||||||
logFileStream
|
logFileStream,
|
||||||
|
jobSessionManager
|
||||||
)
|
)
|
||||||
|
|
||||||
currentState = result.state
|
currentState = result.state
|
||||||
@@ -158,7 +161,8 @@ export async function pollJobState(
|
|||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
authConfig,
|
authConfig,
|
||||||
streamLog,
|
streamLog,
|
||||||
logFileStream
|
logFileStream,
|
||||||
|
jobSessionManager
|
||||||
)
|
)
|
||||||
|
|
||||||
currentState = result.state
|
currentState = result.state
|
||||||
@@ -208,7 +212,21 @@ const needsRetry = (state: string) =>
|
|||||||
state === JobState.Pending ||
|
state === JobState.Pending ||
|
||||||
state === JobState.Unavailable
|
state === JobState.Unavailable
|
||||||
|
|
||||||
const doPoll = async (
|
/**
|
||||||
|
* Polls job state.
|
||||||
|
* @param requestClient - the pre-configured HTTP request client.
|
||||||
|
* @param postedJob - the relative or absolute path to the job.
|
||||||
|
* @param currentState - current job state.
|
||||||
|
* @param debug - sets the _debug flag in the job arguments.
|
||||||
|
* @param pollCount - current poll count.
|
||||||
|
* @param pollOptions - an object containing maxPollCount, pollInterval, streamLog and logFolderPath.
|
||||||
|
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||||
|
* @param streamLog - indicates if job log should be streamed.
|
||||||
|
* @param logStream - job log stream.
|
||||||
|
* @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state. Session state is considered healthy if it is equal to 'running' or 'idle'.
|
||||||
|
* @returns - a promise which resolves with a job state
|
||||||
|
*/
|
||||||
|
export const doPoll = async (
|
||||||
requestClient: RequestClient,
|
requestClient: RequestClient,
|
||||||
postedJob: Job,
|
postedJob: Job,
|
||||||
currentState: JobState,
|
currentState: JobState,
|
||||||
@@ -217,7 +235,8 @@ const doPoll = async (
|
|||||||
pollOptions: PollOptions,
|
pollOptions: PollOptions,
|
||||||
authConfig?: AuthConfig,
|
authConfig?: AuthConfig,
|
||||||
streamLog?: boolean,
|
streamLog?: boolean,
|
||||||
logStream?: WriteStream
|
logStream?: WriteStream,
|
||||||
|
jobSessionManager?: JobSessionManager
|
||||||
): Promise<{ state: JobState; pollCount: number }> => {
|
): Promise<{ state: JobState; pollCount: number }> => {
|
||||||
const { maxPollCount, pollInterval } = pollOptions
|
const { maxPollCount, pollInterval } = pollOptions
|
||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
@@ -229,6 +248,40 @@ const doPoll = async (
|
|||||||
let startLogLine = 0
|
let startLogLine = 0
|
||||||
|
|
||||||
while (needsRetry(state) && pollCount <= maxPollCount) {
|
while (needsRetry(state) && pollCount <= maxPollCount) {
|
||||||
|
// Check parent session state on every 10th job state poll.
|
||||||
|
if (jobSessionManager && pollCount && pollCount % 10 === 0 && authConfig) {
|
||||||
|
const { session, sessionManager } = jobSessionManager
|
||||||
|
const { stateUrl, etag, id: sessionId } = session
|
||||||
|
const { access_token } = authConfig
|
||||||
|
const { id: jobId } = postedJob
|
||||||
|
|
||||||
|
// Get session state.
|
||||||
|
const { result: sessionState, responseStatus } = await sessionManager[
|
||||||
|
'getSessionState'
|
||||||
|
](stateUrl, etag, access_token).catch((err) => {
|
||||||
|
// Handle error while getting session state.
|
||||||
|
throw new JobStatePollError(jobId, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Checks if session state is equal to 'running' or 'idle'.
|
||||||
|
const isSessionStatesHealthy = (state: string) =>
|
||||||
|
[SessionState.Running, SessionState.Idle].includes(
|
||||||
|
state as SessionState
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clear parent session and throw an error if session state is not
|
||||||
|
// 'running', 'idle' or response status is not 200.
|
||||||
|
if (!isSessionStatesHealthy(sessionState) || responseStatus !== 200) {
|
||||||
|
sessionManager.clearSession(sessionId, access_token)
|
||||||
|
|
||||||
|
const sessionError = isSessionStatesHealthy(sessionState)
|
||||||
|
? `Session response status is not 200. Session response status is ${responseStatus}.`
|
||||||
|
: `Session state of the job is not 'running' or 'idle'. Session state is '${sessionState}'`
|
||||||
|
|
||||||
|
throw new JobStatePollError(jobId, new Error(sessionError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state = await getJobState(
|
state = await getJobState(
|
||||||
requestClient,
|
requestClient,
|
||||||
postedJob,
|
postedJob,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { RequestClient } from '../../../request/RequestClient'
|
import { RequestClient } from '../../../request/RequestClient'
|
||||||
import { SessionManager } from '../../../SessionManager'
|
import { SessionManager } from '../../../SessionManager'
|
||||||
import { executeScript } from '../executeScript'
|
import { executeOnComputeApi } from '../executeOnComputeApi'
|
||||||
import { mockSession, mockAuthConfig, mockJob } from './mockResponses'
|
import { mockSession, mockAuthConfig, mockJob } from './mockResponses'
|
||||||
import * as pollJobStateModule from '../pollJobState'
|
import * as pollJobStateModule from '../pollJobState'
|
||||||
import * as uploadTablesModule from '../uploadTables'
|
import * as uploadTablesModule from '../uploadTables'
|
||||||
import * as getTokensModule from '../../../auth/getTokens'
|
import * as getTokensModule from '../../../auth/getTokens'
|
||||||
import * as formatDataModule from '../../../utils/formatDataForRequest'
|
import * as formatDataModule from '../../../utils/formatDataForRequest'
|
||||||
import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
||||||
import { PollOptions } from '../../../types'
|
import { PollOptions, JobSessionManager } from '../../../types'
|
||||||
import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors'
|
import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors'
|
||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not try to get fresh tokens if an authConfig is not provided', async () => {
|
it('should not try to get fresh tokens if an authConfig is not provided', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -38,7 +38,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should try to get fresh tokens if an authConfig is provided', async () => {
|
it('should try to get fresh tokens if an authConfig is provided', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -55,7 +55,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should get a session from the session manager before executing', async () => {
|
it('should get a session from the session manager before executing', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -72,7 +72,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(sessionManager, 'getSession')
|
.spyOn(sessionManager, 'getSession')
|
||||||
.mockImplementation(() => Promise.reject('Test Error'))
|
.mockImplementation(() => Promise.reject('Test Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -85,7 +85,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fetch the PID when printPid is true', async () => {
|
it('should fetch the PID when printPid is true', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -113,7 +113,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(sessionManager, 'getVariable')
|
.spyOn(sessionManager, 'getVariable')
|
||||||
.mockImplementation(() => Promise.reject('Test Error'))
|
.mockImplementation(() => Promise.reject('Test Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -139,7 +139,7 @@ describe('executeScript', () => {
|
|||||||
Promise.resolve([{ tableName: 'test', file: { id: 1 } }])
|
Promise.resolve([{ tableName: 'test', file: { id: 1 } }])
|
||||||
)
|
)
|
||||||
|
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -163,7 +163,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should format data as CSV when it does not contain semicolons', async () => {
|
it('should format data as CSV when it does not contain semicolons', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -189,7 +189,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(formatDataModule, 'formatDataForRequest')
|
.spyOn(formatDataModule, 'formatDataForRequest')
|
||||||
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
||||||
|
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -217,14 +217,7 @@ describe('executeScript', () => {
|
|||||||
sasjs_tables: 'foo',
|
sasjs_tables: 'foo',
|
||||||
sasjs0data: 'bar'
|
sasjs0data: 'bar'
|
||||||
},
|
},
|
||||||
arguments: {
|
version: 2
|
||||||
_contextName: 'test context',
|
|
||||||
_OMITJSONLISTING: true,
|
|
||||||
_OMITJSONLOG: true,
|
|
||||||
_OMITSESSIONRESULTS: true,
|
|
||||||
_OMITTEXTLISTING: true,
|
|
||||||
_OMITTEXTLOG: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mockAuthConfig.access_token
|
mockAuthConfig.access_token
|
||||||
)
|
)
|
||||||
@@ -235,7 +228,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(formatDataModule, 'formatDataForRequest')
|
.spyOn(formatDataModule, 'formatDataForRequest')
|
||||||
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
||||||
|
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -264,14 +257,7 @@ describe('executeScript', () => {
|
|||||||
sasjs0data: 'bar',
|
sasjs0data: 'bar',
|
||||||
_DEBUG: 131
|
_DEBUG: 131
|
||||||
},
|
},
|
||||||
arguments: {
|
version: 2
|
||||||
_contextName: 'test context',
|
|
||||||
_OMITJSONLISTING: true,
|
|
||||||
_OMITJSONLOG: true,
|
|
||||||
_OMITSESSIONRESULTS: false,
|
|
||||||
_OMITTEXTLISTING: true,
|
|
||||||
_OMITTEXTLOG: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mockAuthConfig.access_token
|
mockAuthConfig.access_token
|
||||||
)
|
)
|
||||||
@@ -282,7 +268,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(requestClient, 'post')
|
.spyOn(requestClient, 'post')
|
||||||
.mockImplementation(() => Promise.reject('Test Error'))
|
.mockImplementation(() => Promise.reject('Test Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -302,7 +288,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should immediately return the session when waitForResult is false', async () => {
|
it('should immediately return the session when waitForResult is false', async () => {
|
||||||
const result = await executeScript(
|
const result = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -322,7 +308,12 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should poll for job completion when waitForResult is true', async () => {
|
it('should poll for job completion when waitForResult is true', async () => {
|
||||||
await executeScript(
|
const jobSessionManager: JobSessionManager = {
|
||||||
|
session: mockSession,
|
||||||
|
sessionManager: sessionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -343,7 +334,8 @@ describe('executeScript', () => {
|
|||||||
mockJob,
|
mockJob,
|
||||||
false,
|
false,
|
||||||
mockAuthConfig,
|
mockAuthConfig,
|
||||||
defaultPollOptions
|
defaultPollOptions,
|
||||||
|
jobSessionManager
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -352,7 +344,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(pollJobStateModule, 'pollJobState')
|
.spyOn(pollJobStateModule, 'pollJobState')
|
||||||
.mockImplementation(() => Promise.reject('Poll Error'))
|
.mockImplementation(() => Promise.reject('Poll Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -378,7 +370,7 @@ describe('executeScript', () => {
|
|||||||
Promise.reject({ response: { data: 'err=5113,' } })
|
Promise.reject({ response: { data: 'err=5113,' } })
|
||||||
)
|
)
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -404,7 +396,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fetch the logs for the job if debug is true and a log URL is available', async () => {
|
it('should fetch the logs for the job if debug is true and a log URL is available', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -429,7 +421,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not fetch the logs for the job if debug is false', async () => {
|
it('should not fetch the logs for the job if debug is false', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -455,7 +447,7 @@ describe('executeScript', () => {
|
|||||||
Promise.resolve(pollJobStateModule.JobState.Failed)
|
Promise.resolve(pollJobStateModule.JobState.Failed)
|
||||||
)
|
)
|
||||||
|
|
||||||
const error: ComputeJobExecutionError = await executeScript(
|
const error: ComputeJobExecutionError = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -490,7 +482,7 @@ describe('executeScript', () => {
|
|||||||
Promise.resolve(pollJobStateModule.JobState.Error)
|
Promise.resolve(pollJobStateModule.JobState.Error)
|
||||||
)
|
)
|
||||||
|
|
||||||
const error: ComputeJobExecutionError = await executeScript(
|
const error: ComputeJobExecutionError = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -519,7 +511,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fetch the result if expectWebout is true', async () => {
|
it('should fetch the result if expectWebout is true', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -550,7 +542,7 @@ describe('executeScript', () => {
|
|||||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -584,7 +576,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should clear the session after execution is complete', async () => {
|
it('should clear the session after execution is complete', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -611,7 +603,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(sessionManager, 'clearSession')
|
.spyOn(sessionManager, 'clearSession')
|
||||||
.mockImplementation(() => Promise.reject('Clear Session Error'))
|
.mockImplementation(() => Promise.reject('Clear Session Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { AuthConfig } from '@sasjs/utils/types'
|
import { AuthConfig } from '@sasjs/utils/types'
|
||||||
import { Job, Session } from '../../../types'
|
import { Job, Session, SessionState } from '../../../types'
|
||||||
|
|
||||||
export const mockSession: Session = {
|
export const mockSession: Session = {
|
||||||
id: 's35510n',
|
id: 's35510n',
|
||||||
state: 'idle',
|
state: SessionState.Idle,
|
||||||
|
stateUrl: '',
|
||||||
links: [],
|
links: [],
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: 1
|
sessionInactiveTimeout: 1
|
||||||
},
|
},
|
||||||
creationTimeStamp: new Date().valueOf().toString()
|
creationTimeStamp: new Date().valueOf().toString(),
|
||||||
|
etag: 'etag-string'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mockJob: Job = {
|
export const mockJob: Job = {
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
import { RequestClient } from '../../../request/RequestClient'
|
import { RequestClient } from '../../../request/RequestClient'
|
||||||
import { mockAuthConfig, mockJob } from './mockResponses'
|
import { mockAuthConfig, mockJob } from './mockResponses'
|
||||||
import { pollJobState } from '../pollJobState'
|
import { pollJobState, doPoll, JobState } from '../pollJobState'
|
||||||
import * as getTokensModule from '../../../auth/getTokens'
|
import * as getTokensModule from '../../../auth/getTokens'
|
||||||
import * as saveLogModule from '../saveLog'
|
import * as saveLogModule from '../saveLog'
|
||||||
import * as getFileStreamModule from '../getFileStream'
|
import * as getFileStreamModule from '../getFileStream'
|
||||||
import * as isNodeModule from '../../../utils/isNode'
|
import * as isNodeModule from '../../../utils/isNode'
|
||||||
import * as delayModule from '../../../utils/delay'
|
import * as delayModule from '../../../utils/delay'
|
||||||
import { PollOptions, PollStrategy } from '../../../types'
|
import {
|
||||||
|
PollOptions,
|
||||||
|
PollStrategy,
|
||||||
|
SessionState,
|
||||||
|
JobSessionManager
|
||||||
|
} from '../../../types'
|
||||||
import { WriteStream } from 'fs'
|
import { WriteStream } from 'fs'
|
||||||
|
import { SessionManager } from '../../../SessionManager'
|
||||||
|
import { JobStatePollError } from '../../../types'
|
||||||
|
|
||||||
const baseUrl = 'http://localhost'
|
const baseUrl = 'http://localhost'
|
||||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||||
|
const sessionManager = new (<jest.Mock<SessionManager>>SessionManager)()
|
||||||
requestClient['httpClient'].defaults.baseURL = baseUrl
|
requestClient['httpClient'].defaults.baseURL = baseUrl
|
||||||
|
|
||||||
const defaultStreamLog = false
|
const defaultStreamLog = false
|
||||||
@@ -423,6 +431,218 @@ describe('pollJobState', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('doPoll', () => {
|
||||||
|
const sessionStateLink = '/compute/sessions/session-id-ses0000/state'
|
||||||
|
const jobSessionManager: JobSessionManager = {
|
||||||
|
sessionManager,
|
||||||
|
session: {
|
||||||
|
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
||||||
|
state: SessionState.NoState,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href: sessionStateLink,
|
||||||
|
method: 'GET',
|
||||||
|
rel: 'state',
|
||||||
|
type: 'text/plain',
|
||||||
|
uri: sessionStateLink
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
sessionInactiveTimeout: 900
|
||||||
|
},
|
||||||
|
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`,
|
||||||
|
stateUrl: '',
|
||||||
|
etag: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should check session state on every 10th job state poll', async () => {
|
||||||
|
const mockedGetSessionState = jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: SessionState.Idle,
|
||||||
|
responseStatus: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let getSessionStateCount = 0
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
getSessionStateCount++
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
result:
|
||||||
|
getSessionStateCount < 20 ? JobState.Running : JobState.Completed,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedGetSessionState).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle error while checking session state', async () => {
|
||||||
|
const sessionStateError = 'Error while getting session state.'
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.reject(sessionStateError)
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: JobState.Running,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
).rejects.toEqual(
|
||||||
|
new JobStatePollError(mockJob.id, new Error(sessionStateError))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error if session state is not healthy', async () => {
|
||||||
|
const filteredSessionStates = Object.values(SessionState).filter(
|
||||||
|
(state) => state !== SessionState.Running && state !== SessionState.Idle
|
||||||
|
)
|
||||||
|
const randomSessionState =
|
||||||
|
filteredSessionStates[
|
||||||
|
Math.floor(Math.random() * filteredSessionStates.length)
|
||||||
|
]
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: randomSessionState,
|
||||||
|
responseStatus: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: JobState.Running,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockedClearSession = jest
|
||||||
|
.spyOn(sessionManager, 'clearSession')
|
||||||
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
).rejects.toEqual(
|
||||||
|
new JobStatePollError(
|
||||||
|
mockJob.id,
|
||||||
|
new Error(
|
||||||
|
`Session state of the job is not 'running' or 'idle'. Session state is '${randomSessionState}'`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedClearSession).toHaveBeenCalledWith(
|
||||||
|
jobSessionManager.session.id,
|
||||||
|
mockAuthConfig.access_token
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle throw an error if response status of session state is not 200', async () => {
|
||||||
|
const sessionStateResponseStatus = 500
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: SessionState.Running,
|
||||||
|
responseStatus: sessionStateResponseStatus
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: JobState.Running,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockedClearSession = jest
|
||||||
|
.spyOn(sessionManager, 'clearSession')
|
||||||
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
).rejects.toEqual(
|
||||||
|
new JobStatePollError(
|
||||||
|
mockJob.id,
|
||||||
|
new Error(
|
||||||
|
`Session response status is not 200. Session response status is ${sessionStateResponseStatus}.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedClearSession).toHaveBeenCalledWith(
|
||||||
|
jobSessionManager.session.id,
|
||||||
|
mockAuthConfig.access_token
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const setupMocks = () => {
|
const setupMocks = () => {
|
||||||
jest.restoreAllMocks()
|
jest.restoreAllMocks()
|
||||||
jest.mock('../../../request/RequestClient')
|
jest.mock('../../../request/RequestClient')
|
||||||
|
|||||||
@@ -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 = ''
|
||||||
@@ -14,6 +15,7 @@ export class AuthManager {
|
|||||||
private loginUrl: string
|
private loginUrl: string
|
||||||
private logoutUrl: string
|
private logoutUrl: string
|
||||||
private redirectedLoginUrl = `/SASLogon` //SAS 9 M8 no longer redirects from `/SASLogon/home` to the login page. `/SASLogon` seems to be stable enough across SAS versions
|
private redirectedLoginUrl = `/SASLogon` //SAS 9 M8 no longer redirects from `/SASLogon/home` to the login page. `/SASLogon` seems to be stable enough across SAS versions
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
private serverType: ServerType,
|
private serverType: ServerType,
|
||||||
@@ -27,6 +29,8 @@ export class AuthManager {
|
|||||||
: this.serverType === ServerType.SasViya
|
: this.serverType === ServerType.SasViya
|
||||||
? '/SASLogon/logout.do?'
|
? '/SASLogon/logout.do?'
|
||||||
: '/SASLogon/logout'
|
: '/SASLogon/logout'
|
||||||
|
|
||||||
|
this.redirectedLoginUrl = this.serverUrl + this.redirectedLoginUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,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)) {
|
||||||
@@ -214,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()
|
||||||
@@ -381,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,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)
|
||||||
@@ -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 }))
|
||||||
|
|
||||||
@@ -365,7 +374,7 @@ describe('AuthManager', () => {
|
|||||||
expect(loginResponse.userName).toEqual(userName)
|
expect(loginResponse.userName).toEqual(userName)
|
||||||
|
|
||||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||||
`/SASLogon`,
|
`${serverUrl}/SASLogon`,
|
||||||
'SASLogon',
|
'SASLogon',
|
||||||
{
|
{
|
||||||
width: 500,
|
width: 500,
|
||||||
@@ -409,7 +418,7 @@ describe('AuthManager', () => {
|
|||||||
expect(loginResponse.userName).toEqual(userName)
|
expect(loginResponse.userName).toEqual(userName)
|
||||||
|
|
||||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||||
`/SASLogon`,
|
`${serverUrl}/SASLogon`,
|
||||||
'SASLogon',
|
'SASLogon',
|
||||||
{
|
{
|
||||||
width: 500,
|
width: 500,
|
||||||
@@ -453,7 +462,7 @@ describe('AuthManager', () => {
|
|||||||
expect(loginResponse.userName).toEqual('')
|
expect(loginResponse.userName).toEqual('')
|
||||||
|
|
||||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||||
`/SASLogon`,
|
`${serverUrl}/SASLogon`,
|
||||||
'SASLogon',
|
'SASLogon',
|
||||||
{
|
{
|
||||||
width: 500,
|
width: 500,
|
||||||
@@ -497,7 +506,7 @@ describe('AuthManager', () => {
|
|||||||
expect(loginResponse.userName).toEqual('')
|
expect(loginResponse.userName).toEqual('')
|
||||||
|
|
||||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||||
`/SASLogon`,
|
`${serverUrl}/SASLogon`,
|
||||||
'SASLogon',
|
'SASLogon',
|
||||||
{
|
{
|
||||||
width: 500,
|
width: 500,
|
||||||
|
|||||||
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',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RequestClient } from '../request/RequestClient'
|
|||||||
import * as dotenv from 'dotenv'
|
import * as dotenv from 'dotenv'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
import { Session, Context } from '../types'
|
import { Session, SessionState, Context } from '../types'
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||||
@@ -11,21 +11,34 @@ const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
|||||||
|
|
||||||
describe('SessionManager', () => {
|
describe('SessionManager', () => {
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
process.env.SERVER_URL = 'https://server.com'
|
||||||
|
|
||||||
const sessionManager = new SessionManager(
|
const sessionManager = new SessionManager(
|
||||||
process.env.SERVER_URL as string,
|
process.env.SERVER_URL as string,
|
||||||
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
||||||
requestClient
|
requestClient
|
||||||
)
|
)
|
||||||
|
const sessionStateLink = '/compute/sessions/session-id-ses0000/state'
|
||||||
|
const sessionEtag = 'etag-string'
|
||||||
|
|
||||||
const getMockSession = () => ({
|
const getMockSession = (): Session => ({
|
||||||
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
||||||
state: '',
|
state: SessionState.NoState,
|
||||||
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
links: [
|
||||||
|
{
|
||||||
|
href: sessionStateLink,
|
||||||
|
method: 'GET',
|
||||||
|
rel: 'state',
|
||||||
|
type: 'text/plain',
|
||||||
|
uri: sessionStateLink
|
||||||
|
}
|
||||||
|
],
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: 900
|
sessionInactiveTimeout: 900
|
||||||
},
|
},
|
||||||
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`
|
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`,
|
||||||
|
stateUrl: sessionStateLink,
|
||||||
|
etag: sessionEtag
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -89,19 +102,21 @@ describe('SessionManager', () => {
|
|||||||
describe('waitForSession', () => {
|
describe('waitForSession', () => {
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
state: '',
|
state: SessionState.NoState,
|
||||||
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: 0
|
sessionInactiveTimeout: 0
|
||||||
},
|
},
|
||||||
creationTimeStamp: ''
|
creationTimeStamp: '',
|
||||||
|
stateUrl: sessionStateLink,
|
||||||
|
etag: sessionEtag
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
;(process as any).logger = new Logger(LogLevel.Off)
|
;(process as any).logger = new Logger(LogLevel.Off)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
|
it('should log http response code and session state if SAS server did not provide session state', async () => {
|
||||||
let requestAttempt = 0
|
let requestAttempt = 0
|
||||||
const requestAttemptLimit = 10
|
const requestAttemptLimit = 10
|
||||||
const sessionState = 'idle'
|
const sessionState = 'idle'
|
||||||
@@ -124,15 +139,17 @@ describe('SessionManager', () => {
|
|||||||
sessionManager['waitForSession'](session, null, 'access_token')
|
sessionManager['waitForSession'](session, null, 'access_token')
|
||||||
).resolves.toEqual(sessionState)
|
).resolves.toEqual(sessionState)
|
||||||
|
|
||||||
|
const sessionStateUrl = process.env.SERVER_URL + session.stateUrl
|
||||||
|
|
||||||
expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit)
|
expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit)
|
||||||
expect((process as any).logger.info).toHaveBeenCalledTimes(3)
|
expect((process as any).logger.info).toHaveBeenCalledTimes(3)
|
||||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
`Polling: ${process.env.SERVER_URL}`
|
`Polling: ${sessionStateUrl}`
|
||||||
)
|
)
|
||||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
`Could not get session state. Server responded with 304 whilst checking state: ${process.env.SERVER_URL}`
|
`Could not get session state. Server responded with 304 whilst checking state: ${sessionStateUrl}`
|
||||||
)
|
)
|
||||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||||
3,
|
3,
|
||||||
@@ -142,7 +159,7 @@ describe('SessionManager', () => {
|
|||||||
|
|
||||||
it('should throw an error if there is no session link', async () => {
|
it('should throw an error if there is no session link', async () => {
|
||||||
const customSession = JSON.parse(JSON.stringify(session))
|
const customSession = JSON.parse(JSON.stringify(session))
|
||||||
customSession.links = []
|
customSession.stateUrl = ''
|
||||||
|
|
||||||
mockedAxios.get.mockImplementation(() =>
|
mockedAxios.get.mockImplementation(() =>
|
||||||
Promise.resolve({ data: customSession.state, status: 200 })
|
Promise.resolve({ data: customSession.state, status: 200 })
|
||||||
@@ -156,6 +173,7 @@ describe('SessionManager', () => {
|
|||||||
it('should throw an error if could not get session state', async () => {
|
it('should throw an error if could not get session state', async () => {
|
||||||
const gettingSessionStatus = 500
|
const gettingSessionStatus = 500
|
||||||
const sessionStatusError = `Getting session status timed out after 60 seconds. Request failed with status code ${gettingSessionStatus}`
|
const sessionStatusError = `Getting session status timed out after 60 seconds. Request failed with status code ${gettingSessionStatus}`
|
||||||
|
const sessionStateUrl = process.env.SERVER_URL + session.stateUrl
|
||||||
|
|
||||||
mockedAxios.get.mockImplementation(() =>
|
mockedAxios.get.mockImplementation(() =>
|
||||||
Promise.reject({
|
Promise.reject({
|
||||||
@@ -168,7 +186,7 @@ describe('SessionManager', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedError = `Error while waiting for session. Error while getting session state. GET request to ${process.env.SERVER_URL}?wait=30 failed with status code ${gettingSessionStatus}. ${sessionStatusError}`
|
const expectedError = `Error while waiting for session. Error while getting session state. GET request to ${sessionStateUrl}?wait=30 failed with status code ${gettingSessionStatus}. ${sessionStatusError}`
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sessionManager['waitForSession'](session, null, 'access_token')
|
sessionManager['waitForSession'](session, null, 'access_token')
|
||||||
@@ -427,4 +445,45 @@ describe('SessionManager', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('createAndWaitForSession', () => {
|
||||||
|
it('should create session with etag and stateUrl', async () => {
|
||||||
|
const etag = sessionEtag
|
||||||
|
const customSession: any = getMockSession()
|
||||||
|
delete customSession.etag
|
||||||
|
delete customSession.stateUrl
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'post').mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
result: customSession,
|
||||||
|
etag
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'setCurrentContext')
|
||||||
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|
||||||
|
sessionManager['currentContext'] = {
|
||||||
|
name: 'context name',
|
||||||
|
id: 'string',
|
||||||
|
createdBy: 'string',
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve({ result: SessionState.Idle, responseStatus: 200 })
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedSession = await sessionManager['createAndWaitForSession']()
|
||||||
|
|
||||||
|
expect(customSession.id).toEqual(expectedSession.id)
|
||||||
|
expect(
|
||||||
|
customSession.links.find((l: any) => l.rel === 'state').href
|
||||||
|
).toEqual(expectedSession.stateUrl)
|
||||||
|
expect(expectedSession.etag).toEqual(etag)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,34 @@
|
|||||||
import { Link } from './Link'
|
import { Link } from './Link'
|
||||||
|
import { SessionManager } from '../SessionManager'
|
||||||
|
|
||||||
|
export enum SessionState {
|
||||||
|
Completed = 'completed',
|
||||||
|
Running = 'running',
|
||||||
|
Pending = 'pending',
|
||||||
|
Idle = 'idle',
|
||||||
|
Unavailable = 'unavailable',
|
||||||
|
NoState = '',
|
||||||
|
Failed = 'failed',
|
||||||
|
Error = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
id: string
|
id: string
|
||||||
state: string
|
state: SessionState
|
||||||
|
stateUrl: string
|
||||||
links: Link[]
|
links: Link[]
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: number
|
sessionInactiveTimeout: number
|
||||||
}
|
}
|
||||||
creationTimeStamp: string
|
creationTimeStamp: string
|
||||||
|
etag: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionVariable {
|
export interface SessionVariable {
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JobSessionManager {
|
||||||
|
session: Session
|
||||||
|
sessionManager: SessionManager
|
||||||
|
}
|
||||||
|
|||||||
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'
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user