mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
1 Commits
v4.10.2
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ae198c27 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -14,7 +14,7 @@ What code changes have been made to achieve the intent.
|
||||
|
||||
No PR (that involves a non-trivial code change) should be merged, unless all items below are confirmed! If an urgent fix is needed - use a tar file.
|
||||
|
||||
- [ ] Unit tests coverage has been increased and a new threshold is set.
|
||||
|
||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||
- (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
|
||||
|
||||
2
.github/workflows/assign-reviewer.yml
vendored
2
.github/workflows/assign-reviewer.yml
vendored
@@ -10,4 +10,4 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: uesteibar/reviewer-lottery@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repo-token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -22,9 +22,8 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
# FIXME: uncomment 'Check npm audit' step after axios version bump
|
||||
# - name: Check npm audit
|
||||
# run: npm audit --production --audit-level=low
|
||||
- name: Check npm audit
|
||||
run: npm audit --production --audit-level=low
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
@@ -81,10 +80,6 @@ jobs:
|
||||
npm run update:adapter
|
||||
pm2 start --name sasjs-test npm -- start
|
||||
|
||||
- name: Sleep for 10 seconds
|
||||
run: sleep 10s
|
||||
shell: bash
|
||||
|
||||
- name: Run cypress on sasjs
|
||||
run: |
|
||||
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
|
||||
|
||||
4
.github/workflows/generateDocs.yml
vendored
4
.github/workflows/generateDocs.yml
vendored
@@ -37,8 +37,8 @@ jobs:
|
||||
- name: Push generated docs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: ./docs
|
||||
publish_dir: ./docs
|
||||
cname: adapter.sasjs.io
|
||||
|
||||
|
||||
2
.github/workflows/npmpublish.yml
vendored
2
.github/workflows/npmpublish.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Send Matrix message
|
||||
|
||||
@@ -151,11 +151,7 @@ The `request()` method also has optional parameters such as a config object and
|
||||
|
||||
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
||||
|
||||
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
||||
|
||||
### Verbose Mode
|
||||
|
||||
Set `verbose` to `true` to enable verbose mode that logs a summary of every HTTP response. Verbose mode can be disabled by calling `disableVerboseMode` method or enabled by `enableVerboseMode` method. Verbose mode also supports `bleached` mode that disables extra colors in req/res summary. To enable `bleached` verbose mode, pass `verbose` equal to `bleached` while instantiating an instance of `RequestClient` or to `setVerboseMode` method. Verbose mode can also be enabled/disabled by `startComputeJob` method.
|
||||
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
||||
|
||||
### Session Manager
|
||||
|
||||
@@ -277,7 +273,6 @@ Configuration on the client side involves passing an object on startup, which ca
|
||||
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server).
|
||||
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
||||
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||
* `verbose` - optional, if `true` then a summary of every HTTP response is logged.
|
||||
* `loginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
|
||||
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
||||
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
||||
|
||||
@@ -41,14 +41,7 @@ module.exports = {
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 64.03,
|
||||
branches: 45.11,
|
||||
functions: 54.18,
|
||||
lines: 64.53
|
||||
}
|
||||
},
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -16800,9 +16800,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -29626,9 +29626,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true
|
||||
},
|
||||
"wordwrap": {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { pollJobState } from './api/viya/pollJobState'
|
||||
import { getTokens } from './auth/getTokens'
|
||||
import { uploadTables } from './api/viya/uploadTables'
|
||||
import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
|
||||
import { executeScript } from './api/viya/executeScript'
|
||||
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
||||
import { refreshTokensForViya } from './auth/refreshTokensForViya'
|
||||
|
||||
@@ -293,7 +293,7 @@ export class SASViyaApiClient {
|
||||
printPid = false,
|
||||
variables?: MacroVar
|
||||
): Promise<any> {
|
||||
return executeOnComputeApi(
|
||||
return executeScript(
|
||||
this.requestClient,
|
||||
this.sessionManager,
|
||||
this.rootFolderName,
|
||||
|
||||
66
src/SASjs.ts
66
src/SASjs.ts
@@ -4,12 +4,7 @@ import {
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism,
|
||||
VerboseMode,
|
||||
ErrorResponse,
|
||||
LoginOptions,
|
||||
LoginResult,
|
||||
ExecutionQuery
|
||||
LoginMechanism
|
||||
} from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
@@ -34,7 +29,8 @@ import {
|
||||
Sas9JobExecutor,
|
||||
FileUploader
|
||||
} from './job-execution'
|
||||
import { AxiosResponse, AxiosError } from 'axios'
|
||||
import { ErrorResponse } from './types/errors'
|
||||
import { LoginOptions, LoginResult } from './types/Login'
|
||||
|
||||
interface ExecuteScriptParams {
|
||||
linesOfCode: string[]
|
||||
@@ -161,23 +157,6 @@ export default class SASjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes job on SASJS server.
|
||||
* @param query - an object containing job path and debug level.
|
||||
* @param appLoc - an application path.
|
||||
* @param authConfig - an object for authentication.
|
||||
* @returns a promise that resolves into job execution result and log.
|
||||
*/
|
||||
public async executeJob(
|
||||
query: ExecutionQuery,
|
||||
appLoc: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
this.isMethodSupported('executeScript', [ServerType.Sasjs])
|
||||
|
||||
return await this.sasJSApiClient?.executeJob(query, appLoc, authConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets compute contexts.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
@@ -875,7 +854,6 @@ export default class SASjs {
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||
* @param variables - an object that represents macro variables.
|
||||
* @param verboseMode - boolean or a string equal to 'bleached' to enable verbose mode (log every HTTP response).
|
||||
*/
|
||||
public async startComputeJob(
|
||||
sasJob: string,
|
||||
@@ -885,8 +863,7 @@ export default class SASjs {
|
||||
waitForResult?: boolean,
|
||||
pollOptions?: PollOptions,
|
||||
printPid = false,
|
||||
variables?: MacroVar,
|
||||
verboseMode?: VerboseMode
|
||||
variables?: MacroVar
|
||||
) {
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
@@ -900,11 +877,6 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
if (verboseMode) {
|
||||
this.requestClient?.setVerboseMode(verboseMode)
|
||||
this.requestClient?.enableVerboseMode()
|
||||
} else if (verboseMode === false) this.requestClient?.disableVerboseMode()
|
||||
|
||||
return this.sasViyaApiClient?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
@@ -998,8 +970,7 @@ export default class SASjs {
|
||||
this.requestClient = new RequestClientClass(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions,
|
||||
this.sasjsConfig.requestHistoryLimit,
|
||||
this.sasjsConfig.verbose
|
||||
this.sasjsConfig.requestHistoryLimit
|
||||
)
|
||||
} else {
|
||||
this.requestClient.setConfig(
|
||||
@@ -1163,31 +1134,4 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables verbose mode that will log a summary of every HTTP response.
|
||||
* @param successCallBack - function that should be triggered on every HTTP response with the status 2**.
|
||||
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
||||
*/
|
||||
public enableVerboseMode(
|
||||
successCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse,
|
||||
errorCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse
|
||||
) {
|
||||
this.requestClient?.enableVerboseMode(successCallBack, errorCallBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off verbose mode to log every HTTP response.
|
||||
*/
|
||||
public disableVerboseMode() {
|
||||
this.requestClient?.disableVerboseMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets verbose mode.
|
||||
* @param verboseMode - value of the verbose mode, can be true, false or bleached(without extra colors).
|
||||
*/
|
||||
public setVerboseMode = (verboseMode: VerboseMode) => {
|
||||
this.requestClient?.setVerboseMode(verboseMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Session, Context, SessionVariable, SessionState } from './types'
|
||||
import { Session, Context, SessionVariable } from './types'
|
||||
import { NoSessionStateError } from './types/errors'
|
||||
import { asyncForEach, isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
@@ -12,7 +12,6 @@ interface ApiErrorResponse {
|
||||
|
||||
export class SessionManager {
|
||||
private loggedErrors: NoSessionStateError[] = []
|
||||
private sessionStateLinkError = 'Error while getting session state link. '
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
@@ -29,7 +28,7 @@ export class SessionManager {
|
||||
private _debug: boolean = false
|
||||
private printedSessionState = {
|
||||
printed: false,
|
||||
state: SessionState.NoState
|
||||
state: ''
|
||||
}
|
||||
|
||||
public get debug() {
|
||||
@@ -266,18 +265,6 @@ 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)
|
||||
|
||||
this.sessions.push(createdSession)
|
||||
@@ -340,30 +327,32 @@ export class SessionManager {
|
||||
etag: string | null,
|
||||
accessToken?: string
|
||||
): Promise<string> {
|
||||
let { state: sessionState } = session
|
||||
const { stateUrl } = session
|
||||
const logger = process.logger || console
|
||||
|
||||
let sessionState = session.state
|
||||
|
||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||
|
||||
if (
|
||||
sessionState === SessionState.Pending ||
|
||||
sessionState === SessionState.Running ||
|
||||
sessionState === SessionState.NoState
|
||||
sessionState === 'pending' ||
|
||||
sessionState === 'running' ||
|
||||
sessionState === ''
|
||||
) {
|
||||
if (stateUrl) {
|
||||
if (stateLink) {
|
||||
if (this.debug && !this.printedSessionState.printed) {
|
||||
logger.info(`Polling: ${this.serverUrl + stateUrl}`)
|
||||
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
|
||||
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
const url = `${this.serverUrl}${stateUrl}?wait=30`
|
||||
const url = `${this.serverUrl}${stateLink.href}?wait=30`
|
||||
|
||||
const { result: state, responseStatus: responseStatus } =
|
||||
await this.getSessionState(url, etag!, accessToken).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while waiting for session. ')
|
||||
})
|
||||
|
||||
sessionState = state.trim() as SessionState
|
||||
sessionState = state.trim()
|
||||
|
||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||
logger.info(`Current session state is '${sessionState}'`)
|
||||
@@ -375,7 +364,7 @@ export class SessionManager {
|
||||
if (!sessionState) {
|
||||
const stateError = new NoSessionStateError(
|
||||
responseStatus,
|
||||
this.serverUrl + stateUrl,
|
||||
this.serverUrl + stateLink.href,
|
||||
session.links.find((l: any) => l.rel === 'log')?.href as string
|
||||
)
|
||||
|
||||
@@ -397,7 +386,7 @@ export class SessionManager {
|
||||
|
||||
return sessionState
|
||||
} else {
|
||||
throw this.sessionStateLinkError
|
||||
throw 'Error while getting session state link. '
|
||||
}
|
||||
} else {
|
||||
this.loggedErrors = []
|
||||
@@ -424,7 +413,7 @@ export class SessionManager {
|
||||
return await this.requestClient
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => ({
|
||||
result: res.result as SessionState,
|
||||
result: res.result as string,
|
||||
responseStatus: res.status
|
||||
}))
|
||||
.catch((err) => {
|
||||
|
||||
@@ -15,12 +15,8 @@ import { formatDataForRequest } from '../../utils/formatDataForRequest'
|
||||
import { pollJobState, JobState } from './pollJobState'
|
||||
import { uploadTables } from './uploadTables'
|
||||
|
||||
interface JobRequestBody {
|
||||
[key: string]: number | string | string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes SAS program on the current SAS Viya server using Compute API.
|
||||
* Executes code on the current SAS Viya server.
|
||||
* @param jobPath - the path to the file being submitted for execution.
|
||||
* @param linesOfCode - an array of code lines to execute.
|
||||
* @param contextName - the context to execute the code in.
|
||||
@@ -33,7 +29,7 @@ interface JobRequestBody {
|
||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||
* @param variables - an object that represents macro variables.
|
||||
*/
|
||||
export async function executeOnComputeApi(
|
||||
export async function executeScript(
|
||||
requestClient: RequestClient,
|
||||
sessionManager: SessionManager,
|
||||
rootFolderName: string,
|
||||
@@ -50,7 +46,6 @@ export async function executeOnComputeApi(
|
||||
variables?: MacroVar
|
||||
): Promise<any> {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(requestClient, authConfig))
|
||||
}
|
||||
@@ -83,13 +78,27 @@ export async function executeOnComputeApi(
|
||||
const logger = process.logger || console
|
||||
|
||||
logger.info(
|
||||
`Triggering '${relativeJobPath}' with PID ${
|
||||
`Triggered '${relativeJobPath}' with PID ${
|
||||
jobIdVariable.value
|
||||
} at ${timestampToYYYYMMDDHHMMSS()}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (isRelativePath(jobPath)) {
|
||||
@@ -98,7 +107,6 @@ export async function executeOnComputeApi(
|
||||
}`
|
||||
} else {
|
||||
const jobPathParts = jobPath.split('/')
|
||||
|
||||
fileName = jobPathParts.pop()
|
||||
}
|
||||
|
||||
@@ -110,6 +118,7 @@ export async function executeOnComputeApi(
|
||||
}
|
||||
|
||||
if (variables) jobVariables = { ...jobVariables, ...variables }
|
||||
|
||||
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
||||
|
||||
let files: any[] = []
|
||||
@@ -136,12 +145,12 @@ export async function executeOnComputeApi(
|
||||
}
|
||||
|
||||
// Execute job in session
|
||||
const jobRequestBody: JobRequestBody = {
|
||||
name: fileName || 'Default Job Name',
|
||||
const jobRequestBody = {
|
||||
name: fileName,
|
||||
description: 'Powered by SASjs',
|
||||
code: linesOfCode,
|
||||
variables: jobVariables,
|
||||
version: 2
|
||||
arguments: jobArguments
|
||||
}
|
||||
|
||||
const { result: postedJob, etag } = await requestClient
|
||||
@@ -170,21 +179,16 @@ export async function executeOnComputeApi(
|
||||
postedJob,
|
||||
debug,
|
||||
authConfig,
|
||||
pollOptions,
|
||||
{
|
||||
session,
|
||||
sessionManager
|
||||
}
|
||||
pollOptions
|
||||
).catch(async (err) => {
|
||||
const error = err?.response?.data
|
||||
const result = /err=[0-9]*,/.exec(error)
|
||||
const errorCode = '5113'
|
||||
|
||||
const errorCode = '5113'
|
||||
if (result?.[0]?.slice(4, -1) === errorCode) {
|
||||
const logCount = 1000000
|
||||
const sessionLogUrl =
|
||||
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
||||
|
||||
const logCount = 1000000
|
||||
err.log = await fetchLogByChunks(
|
||||
requestClient,
|
||||
access_token!,
|
||||
@@ -192,7 +196,6 @@ export async function executeOnComputeApi(
|
||||
logCount
|
||||
)
|
||||
}
|
||||
|
||||
throw prefixMessage(err, 'Error while polling job status. ')
|
||||
})
|
||||
|
||||
@@ -211,12 +214,12 @@ export async function executeOnComputeApi(
|
||||
|
||||
let jobResult
|
||||
let log = ''
|
||||
|
||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||
|
||||
if (debug && logLink) {
|
||||
const logUrl = `${logLink.href}/content`
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
|
||||
log = await fetchLogByChunks(
|
||||
requestClient,
|
||||
access_token!,
|
||||
@@ -229,7 +232,9 @@ export async function executeOnComputeApi(
|
||||
throw new ComputeJobExecutionError(currentJob, log)
|
||||
}
|
||||
|
||||
if (!expectWebout) return { job: currentJob, log }
|
||||
if (!expectWebout) {
|
||||
return { job: currentJob, log }
|
||||
}
|
||||
|
||||
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
||||
|
||||
@@ -240,7 +245,6 @@ export async function executeOnComputeApi(
|
||||
if (logLink) {
|
||||
const logUrl = `${logLink.href}/content`
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
|
||||
log = await fetchLogByChunks(
|
||||
requestClient,
|
||||
access_token!,
|
||||
@@ -275,7 +279,7 @@ export async function executeOnComputeApi(
|
||||
const error = e as HttpError
|
||||
|
||||
if (error.status === 404) {
|
||||
return executeOnComputeApi(
|
||||
return executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
rootFolderName,
|
||||
@@ -3,7 +3,7 @@ import { Job, PollOptions, PollStrategy } from '../..'
|
||||
import { getTokens } from '../../auth/getTokens'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { JobStatePollError } from '../../types/errors'
|
||||
import { Link, WriteStream, SessionState, JobSessionManager } from '../../types'
|
||||
import { Link, WriteStream } from '../../types'
|
||||
import { delay, isNode } from '../../utils'
|
||||
|
||||
export enum JobState {
|
||||
@@ -37,7 +37,6 @@ export enum JobState {
|
||||
* { 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))
|
||||
* ]
|
||||
* @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
|
||||
*/
|
||||
export async function pollJobState(
|
||||
@@ -45,8 +44,7 @@ export async function pollJobState(
|
||||
postedJob: Job,
|
||||
debug: boolean,
|
||||
authConfig?: AuthConfig,
|
||||
pollOptions?: PollOptions,
|
||||
jobSessionManager?: JobSessionManager
|
||||
pollOptions?: PollOptions
|
||||
): Promise<JobState> {
|
||||
const logger = process.logger || console
|
||||
|
||||
@@ -129,8 +127,7 @@ export async function pollJobState(
|
||||
pollOptions,
|
||||
authConfig,
|
||||
streamLog,
|
||||
logFileStream,
|
||||
jobSessionManager
|
||||
logFileStream
|
||||
)
|
||||
|
||||
currentState = result.state
|
||||
@@ -161,8 +158,7 @@ export async function pollJobState(
|
||||
defaultPollOptions,
|
||||
authConfig,
|
||||
streamLog,
|
||||
logFileStream,
|
||||
jobSessionManager
|
||||
logFileStream
|
||||
)
|
||||
|
||||
currentState = result.state
|
||||
@@ -212,21 +208,7 @@ const needsRetry = (state: string) =>
|
||||
state === JobState.Pending ||
|
||||
state === JobState.Unavailable
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
const doPoll = async (
|
||||
requestClient: RequestClient,
|
||||
postedJob: Job,
|
||||
currentState: JobState,
|
||||
@@ -235,8 +217,7 @@ export const doPoll = async (
|
||||
pollOptions: PollOptions,
|
||||
authConfig?: AuthConfig,
|
||||
streamLog?: boolean,
|
||||
logStream?: WriteStream,
|
||||
jobSessionManager?: JobSessionManager
|
||||
logStream?: WriteStream
|
||||
): Promise<{ state: JobState; pollCount: number }> => {
|
||||
const { maxPollCount, pollInterval } = pollOptions
|
||||
const logger = process.logger || console
|
||||
@@ -248,40 +229,6 @@ export const doPoll = async (
|
||||
let startLogLine = 0
|
||||
|
||||
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(
|
||||
requestClient,
|
||||
postedJob,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { RequestClient } from '../../../request/RequestClient'
|
||||
import { SessionManager } from '../../../SessionManager'
|
||||
import { executeOnComputeApi } from '../executeOnComputeApi'
|
||||
import { executeScript } from '../executeScript'
|
||||
import { mockSession, mockAuthConfig, mockJob } from './mockResponses'
|
||||
import * as pollJobStateModule from '../pollJobState'
|
||||
import * as uploadTablesModule from '../uploadTables'
|
||||
import * as getTokensModule from '../../../auth/getTokens'
|
||||
import * as formatDataModule from '../../../utils/formatDataForRequest'
|
||||
import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
||||
import { PollOptions, JobSessionManager } from '../../../types'
|
||||
import { PollOptions } from '../../../types'
|
||||
import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors'
|
||||
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 () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -38,7 +38,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should try to get fresh tokens if an authConfig is provided', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -55,7 +55,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should get a session from the session manager before executing', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -72,7 +72,7 @@ describe('executeScript', () => {
|
||||
.spyOn(sessionManager, 'getSession')
|
||||
.mockImplementation(() => Promise.reject('Test Error'))
|
||||
|
||||
const error = await executeOnComputeApi(
|
||||
const error = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -85,7 +85,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should fetch the PID when printPid is true', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -113,7 +113,7 @@ describe('executeScript', () => {
|
||||
.spyOn(sessionManager, 'getVariable')
|
||||
.mockImplementation(() => Promise.reject('Test Error'))
|
||||
|
||||
const error = await executeOnComputeApi(
|
||||
const error = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -139,7 +139,7 @@ describe('executeScript', () => {
|
||||
Promise.resolve([{ tableName: 'test', file: { id: 1 } }])
|
||||
)
|
||||
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -163,7 +163,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should format data as CSV when it does not contain semicolons', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -189,7 +189,7 @@ describe('executeScript', () => {
|
||||
.spyOn(formatDataModule, 'formatDataForRequest')
|
||||
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
||||
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -217,7 +217,14 @@ describe('executeScript', () => {
|
||||
sasjs_tables: 'foo',
|
||||
sasjs0data: 'bar'
|
||||
},
|
||||
version: 2
|
||||
arguments: {
|
||||
_contextName: 'test context',
|
||||
_OMITJSONLISTING: true,
|
||||
_OMITJSONLOG: true,
|
||||
_OMITSESSIONRESULTS: true,
|
||||
_OMITTEXTLISTING: true,
|
||||
_OMITTEXTLOG: true
|
||||
}
|
||||
},
|
||||
mockAuthConfig.access_token
|
||||
)
|
||||
@@ -228,7 +235,7 @@ describe('executeScript', () => {
|
||||
.spyOn(formatDataModule, 'formatDataForRequest')
|
||||
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
||||
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -257,7 +264,14 @@ describe('executeScript', () => {
|
||||
sasjs0data: 'bar',
|
||||
_DEBUG: 131
|
||||
},
|
||||
version: 2
|
||||
arguments: {
|
||||
_contextName: 'test context',
|
||||
_OMITJSONLISTING: true,
|
||||
_OMITJSONLOG: true,
|
||||
_OMITSESSIONRESULTS: false,
|
||||
_OMITTEXTLISTING: true,
|
||||
_OMITTEXTLOG: false
|
||||
}
|
||||
},
|
||||
mockAuthConfig.access_token
|
||||
)
|
||||
@@ -268,7 +282,7 @@ describe('executeScript', () => {
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() => Promise.reject('Test Error'))
|
||||
|
||||
const error = await executeOnComputeApi(
|
||||
const error = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -288,7 +302,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should immediately return the session when waitForResult is false', async () => {
|
||||
const result = await executeOnComputeApi(
|
||||
const result = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -308,12 +322,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should poll for job completion when waitForResult is true', async () => {
|
||||
const jobSessionManager: JobSessionManager = {
|
||||
session: mockSession,
|
||||
sessionManager: sessionManager
|
||||
}
|
||||
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -334,8 +343,7 @@ describe('executeScript', () => {
|
||||
mockJob,
|
||||
false,
|
||||
mockAuthConfig,
|
||||
defaultPollOptions,
|
||||
jobSessionManager
|
||||
defaultPollOptions
|
||||
)
|
||||
})
|
||||
|
||||
@@ -344,7 +352,7 @@ describe('executeScript', () => {
|
||||
.spyOn(pollJobStateModule, 'pollJobState')
|
||||
.mockImplementation(() => Promise.reject('Poll Error'))
|
||||
|
||||
const error = await executeOnComputeApi(
|
||||
const error = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -370,7 +378,7 @@ describe('executeScript', () => {
|
||||
Promise.reject({ response: { data: 'err=5113,' } })
|
||||
)
|
||||
|
||||
const error = await executeOnComputeApi(
|
||||
const error = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -396,7 +404,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should fetch the logs for the job if debug is true and a log URL is available', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -421,7 +429,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should not fetch the logs for the job if debug is false', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -447,7 +455,7 @@ describe('executeScript', () => {
|
||||
Promise.resolve(pollJobStateModule.JobState.Failed)
|
||||
)
|
||||
|
||||
const error: ComputeJobExecutionError = await executeOnComputeApi(
|
||||
const error: ComputeJobExecutionError = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -482,7 +490,7 @@ describe('executeScript', () => {
|
||||
Promise.resolve(pollJobStateModule.JobState.Error)
|
||||
)
|
||||
|
||||
const error: ComputeJobExecutionError = await executeOnComputeApi(
|
||||
const error: ComputeJobExecutionError = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -511,7 +519,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should fetch the result if expectWebout is true', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -542,7 +550,7 @@ describe('executeScript', () => {
|
||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||
})
|
||||
|
||||
const error = await executeOnComputeApi(
|
||||
const error = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -576,7 +584,7 @@ describe('executeScript', () => {
|
||||
})
|
||||
|
||||
it('should clear the session after execution is complete', async () => {
|
||||
await executeOnComputeApi(
|
||||
await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
@@ -603,7 +611,7 @@ describe('executeScript', () => {
|
||||
.spyOn(sessionManager, 'clearSession')
|
||||
.mockImplementation(() => Promise.reject('Clear Session Error'))
|
||||
|
||||
const error = await executeOnComputeApi(
|
||||
const error = await executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
'test',
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
import { Job, Session, SessionState } from '../../../types'
|
||||
import { Job, Session } from '../../../types'
|
||||
|
||||
export const mockSession: Session = {
|
||||
id: 's35510n',
|
||||
state: SessionState.Idle,
|
||||
stateUrl: '',
|
||||
state: 'idle',
|
||||
links: [],
|
||||
attributes: {
|
||||
sessionInactiveTimeout: 1
|
||||
},
|
||||
creationTimeStamp: new Date().valueOf().toString(),
|
||||
etag: 'etag-string'
|
||||
creationTimeStamp: new Date().valueOf().toString()
|
||||
}
|
||||
|
||||
export const mockJob: Job = {
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { RequestClient } from '../../../request/RequestClient'
|
||||
import { mockAuthConfig, mockJob } from './mockResponses'
|
||||
import { pollJobState, doPoll, JobState } from '../pollJobState'
|
||||
import { pollJobState } from '../pollJobState'
|
||||
import * as getTokensModule from '../../../auth/getTokens'
|
||||
import * as saveLogModule from '../saveLog'
|
||||
import * as getFileStreamModule from '../getFileStream'
|
||||
import * as isNodeModule from '../../../utils/isNode'
|
||||
import * as delayModule from '../../../utils/delay'
|
||||
import {
|
||||
PollOptions,
|
||||
PollStrategy,
|
||||
SessionState,
|
||||
JobSessionManager
|
||||
} from '../../../types'
|
||||
import { PollOptions, PollStrategy } from '../../../types'
|
||||
import { WriteStream } from 'fs'
|
||||
import { SessionManager } from '../../../SessionManager'
|
||||
import { JobStatePollError } from '../../../types'
|
||||
|
||||
const baseUrl = 'http://localhost'
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
const sessionManager = new (<jest.Mock<SessionManager>>SessionManager)()
|
||||
requestClient['httpClient'].defaults.baseURL = baseUrl
|
||||
|
||||
const defaultStreamLog = false
|
||||
@@ -284,76 +276,6 @@ describe('pollJobState', () => {
|
||||
expect(delays).toEqual([pollIntervals[0], ...pollIntervals])
|
||||
})
|
||||
|
||||
it('should change default poll strategies after completing provided poll options', async () => {
|
||||
const delays: number[] = []
|
||||
|
||||
jest.spyOn(delayModule, 'delay').mockImplementation((ms: number) => {
|
||||
delays.push(ms)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
const customPollOptions: PollOptions = {
|
||||
maxPollCount: 0,
|
||||
pollInterval: 0
|
||||
}
|
||||
|
||||
const requests = [
|
||||
{ maxPollCount: 202, pollInterval: 300 },
|
||||
{ maxPollCount: 300, pollInterval: 3000 },
|
||||
{ maxPollCount: 500, pollInterval: 30000 },
|
||||
{ maxPollCount: 3400, pollInterval: 60000 }
|
||||
]
|
||||
|
||||
// ~200 requests with delay 300ms
|
||||
let request = requests.splice(0, 1)[0]
|
||||
let { maxPollCount, pollInterval } = request
|
||||
|
||||
// should be only one interval because maxPollCount is equal to 0
|
||||
const pollIntervals = [customPollOptions.pollInterval]
|
||||
|
||||
pollIntervals.push(...Array(maxPollCount - 2).fill(pollInterval))
|
||||
|
||||
// ~300 requests with delay 3000
|
||||
request = requests.splice(0, 1)[0]
|
||||
let newAmount = request.maxPollCount
|
||||
pollInterval = request.pollInterval
|
||||
|
||||
pollIntervals.push(...Array(newAmount - maxPollCount).fill(pollInterval))
|
||||
pollIntervals.push(...Array(2).fill(pollInterval))
|
||||
|
||||
// ~500 requests with delay 30000
|
||||
request = requests.splice(0, 1)[0]
|
||||
|
||||
let oldAmount = newAmount
|
||||
newAmount = request.maxPollCount
|
||||
pollInterval = request.pollInterval
|
||||
|
||||
pollIntervals.push(...Array(newAmount - oldAmount - 2).fill(pollInterval))
|
||||
pollIntervals.push(...Array(2).fill(pollInterval))
|
||||
|
||||
// ~3400 requests with delay 60000
|
||||
request = requests.splice(0, 1)[0]
|
||||
|
||||
oldAmount = newAmount
|
||||
newAmount = request.maxPollCount
|
||||
pollInterval = request.pollInterval
|
||||
|
||||
mockSimplePoll(newAmount)
|
||||
|
||||
pollIntervals.push(...Array(newAmount - oldAmount - 2).fill(pollInterval))
|
||||
|
||||
await pollJobState(
|
||||
requestClient,
|
||||
mockJob,
|
||||
false,
|
||||
undefined,
|
||||
customPollOptions
|
||||
)
|
||||
|
||||
expect(delays).toEqual(pollIntervals)
|
||||
})
|
||||
|
||||
it('should throw an error if not valid poll strategies provided', async () => {
|
||||
// INFO: 'maxPollCount' has to be > 0
|
||||
let invalidPollStrategy = {
|
||||
@@ -431,218 +353,6 @@ 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 = () => {
|
||||
jest.restoreAllMocks()
|
||||
jest.mock('../../../request/RequestClient')
|
||||
|
||||
@@ -14,7 +14,6 @@ export class AuthManager {
|
||||
private loginUrl: 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
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private serverType: ServerType,
|
||||
@@ -28,8 +27,6 @@ export class AuthManager {
|
||||
: this.serverType === ServerType.SasViya
|
||||
? '/SASLogon/logout.do?'
|
||||
: '/SASLogon/logout'
|
||||
|
||||
this.redirectedLoginUrl = this.serverUrl + this.redirectedLoginUrl
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -365,7 +365,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`${serverUrl}/SASLogon`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
@@ -409,7 +409,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`${serverUrl}/SASLogon`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
@@ -453,7 +453,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`${serverUrl}/SASLogon`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
@@ -497,7 +497,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`${serverUrl}/SASLogon`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
getValidJson,
|
||||
parseSasViyaDebugResponse,
|
||||
parseWeboutResponse
|
||||
parseWeboutResponse,
|
||||
SASJS_LOGS_SEPARATOR
|
||||
} from '../utils'
|
||||
import { UploadFile } from '../types/UploadFile'
|
||||
import {
|
||||
|
||||
@@ -233,8 +233,7 @@ export default class SASjs {
|
||||
this.requestClient = new RequestClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions,
|
||||
this.sasjsConfig.requestHistoryLimit,
|
||||
this.sasjsConfig.verbose
|
||||
this.sasjsConfig.requestHistoryLimit
|
||||
)
|
||||
} else {
|
||||
this.requestClient.setConfig(
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import {
|
||||
isRelativePath,
|
||||
parseSasViyaDebugResponse,
|
||||
appendExtraResponseAttributes,
|
||||
convertToCSV
|
||||
} from '../../utils'
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
AxiosError,
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse
|
||||
} from 'axios'
|
||||
import axios from 'axios'
|
||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import * as https from 'https'
|
||||
import { CsrfToken } from '..'
|
||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||
@@ -16,7 +10,7 @@ import {
|
||||
JobExecutionError,
|
||||
CertificateError
|
||||
} from '../types/errors'
|
||||
import { SASjsRequest, HttpClient, VerboseMode } from '../types'
|
||||
import { SASjsRequest } from '../types'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
||||
@@ -26,13 +20,45 @@ import {
|
||||
createAxiosInstance
|
||||
} from '../utils'
|
||||
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
||||
import { inspect } from 'util'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
put<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
delete<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
|
||||
saveLocalStorageToken(accessToken: string, refreshToken: string): void
|
||||
clearCsrfTokens(): void
|
||||
clearLocalStorageTokens(): void
|
||||
getBaseUrl(): string
|
||||
}
|
||||
|
||||
export class RequestClient implements HttpClient {
|
||||
private requests: SASjsRequest[] = []
|
||||
private requestsLimit: number = 10
|
||||
private httpInterceptor?: number
|
||||
private verboseMode: VerboseMode = false
|
||||
|
||||
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||
protected fileUploadCsrfToken: CsrfToken | undefined
|
||||
@@ -41,17 +67,10 @@ export class RequestClient implements HttpClient {
|
||||
constructor(
|
||||
protected baseUrl: string,
|
||||
httpsAgentOptions?: https.AgentOptions,
|
||||
requestsLimit?: number,
|
||||
verboseMode?: VerboseMode
|
||||
requestsLimit?: number
|
||||
) {
|
||||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||||
|
||||
if (requestsLimit) this.requestsLimit = requestsLimit
|
||||
|
||||
if (verboseMode) {
|
||||
this.setVerboseMode(verboseMode)
|
||||
this.enableVerboseMode()
|
||||
}
|
||||
}
|
||||
|
||||
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
||||
@@ -71,7 +90,6 @@ export class RequestClient implements HttpClient {
|
||||
this.csrfToken = { headerName: '', value: '' }
|
||||
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
||||
}
|
||||
|
||||
public clearLocalStorageTokens() {
|
||||
localStorage.setItem('accessToken', '')
|
||||
localStorage.setItem('refreshToken', '')
|
||||
@@ -162,7 +180,6 @@ export class RequestClient implements HttpClient {
|
||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||
withCredentials: true
|
||||
}
|
||||
|
||||
if (contentType === 'text/plain') {
|
||||
requestConfig.transformResponse = undefined
|
||||
}
|
||||
@@ -372,181 +389,6 @@ export class RequestClient implements HttpClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds colors to the string.
|
||||
* If verboseMode is set to 'bleached', colors should be disabled
|
||||
* @param str - string to be prettified.
|
||||
* @returns - prettified string
|
||||
*/
|
||||
private prettifyString = (str: any) =>
|
||||
inspect(str, { colors: this.verboseMode !== 'bleached' })
|
||||
|
||||
/**
|
||||
* Formats HTTP request/response body.
|
||||
* @param body - HTTP request/response body.
|
||||
* @returns - formatted string.
|
||||
*/
|
||||
private parseInterceptedBody = (body: any) => {
|
||||
if (!body) return ''
|
||||
|
||||
let parsedBody
|
||||
|
||||
// Tries to parse body into JSON object.
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
parsedBody = JSON.parse(body)
|
||||
} catch (error) {
|
||||
parsedBody = body
|
||||
}
|
||||
} else {
|
||||
parsedBody = body
|
||||
}
|
||||
|
||||
const bodyLines = this.prettifyString(parsedBody).split('\n')
|
||||
|
||||
// Leaves first 50 lines
|
||||
if (bodyLines.length > 51) {
|
||||
bodyLines.splice(50)
|
||||
bodyLines.push('...')
|
||||
}
|
||||
|
||||
return bodyLines.join('\n')
|
||||
}
|
||||
|
||||
private defaultInterceptionCallBack = (
|
||||
axiosResponse: AxiosResponse | AxiosError
|
||||
) => {
|
||||
// Message indicating absent value.
|
||||
const noValueMessage = 'Not provided'
|
||||
|
||||
// Fallback request object that can be safely used to form request summary.
|
||||
type FallbackRequest = { _header?: string; res: { rawHeaders: string[] } }
|
||||
// _header is not present in responses with status 1**
|
||||
// rawHeaders are not present in responses with status 1**
|
||||
let fallbackRequest: FallbackRequest = {
|
||||
_header: `${noValueMessage}\n`,
|
||||
res: { rawHeaders: [noValueMessage] }
|
||||
}
|
||||
|
||||
// Fallback response object that can be safely used to form response summary.
|
||||
type FallbackResponse = {
|
||||
status?: number | string
|
||||
request?: FallbackRequest
|
||||
config: { data?: string }
|
||||
data?: unknown
|
||||
}
|
||||
let fallbackResponse: FallbackResponse = axiosResponse
|
||||
|
||||
if (axios.isAxiosError(axiosResponse)) {
|
||||
const { response, request, config } = axiosResponse
|
||||
|
||||
// Try to use axiosResponse.response to form response summary.
|
||||
if (response) {
|
||||
fallbackResponse = response
|
||||
} else {
|
||||
// Try to use axiosResponse.request to form request summary.
|
||||
if (request) {
|
||||
const { _header, _currentRequest } = request
|
||||
|
||||
// Try to use axiosResponse.request._header to form request summary.
|
||||
if (_header) {
|
||||
fallbackRequest._header = _header
|
||||
}
|
||||
// Try to use axiosResponse.request._currentRequest._header to form request summary.
|
||||
else if (_currentRequest && _currentRequest._header) {
|
||||
fallbackRequest._header = _currentRequest._header
|
||||
}
|
||||
|
||||
const { res } = request
|
||||
|
||||
// Try to use axiosResponse.request.res.rawHeaders to form request summary.
|
||||
if (res && res.rawHeaders) {
|
||||
fallbackRequest.res.rawHeaders = res.rawHeaders
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback config that can be safely used to form response summary.
|
||||
const fallbackConfig = { data: noValueMessage }
|
||||
|
||||
fallbackResponse = {
|
||||
status: noValueMessage,
|
||||
request: fallbackRequest,
|
||||
config: config || fallbackConfig,
|
||||
data: noValueMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { status, config, request, data: resData } = fallbackResponse
|
||||
const { data: reqData } = config
|
||||
const { _header: reqHeaders, res } = request || fallbackRequest
|
||||
const { rawHeaders } = res
|
||||
|
||||
// Converts an array of strings into a single string with the following format:
|
||||
// <headerName>: <headerValue>
|
||||
const resHeaders = rawHeaders.reduce(
|
||||
(acc: string, value: string, i: number) => {
|
||||
if (i % 2 === 0) {
|
||||
acc += `${i === 0 ? '' : '\n'}${value}`
|
||||
} else {
|
||||
acc += `: ${value}`
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
''
|
||||
)
|
||||
|
||||
const parsedResBody = this.parseInterceptedBody(resData)
|
||||
|
||||
// HTTP response summary.
|
||||
process.logger?.info(`HTTP Request (first 50 lines):
|
||||
${reqHeaders}${this.parseInterceptedBody(reqData)}
|
||||
|
||||
HTTP Response Code: ${this.prettifyString(status)}
|
||||
|
||||
HTTP Response (first 50 lines):
|
||||
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
||||
`)
|
||||
|
||||
return axiosResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets verbose mode.
|
||||
* @param verboseMode - value of the verbose mode, can be true, false or bleached(without extra colors).
|
||||
*/
|
||||
public setVerboseMode = (verboseMode: VerboseMode) => {
|
||||
this.verboseMode = verboseMode
|
||||
|
||||
if (this.verboseMode) this.enableVerboseMode()
|
||||
else this.disableVerboseMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on verbose mode to log every HTTP response.
|
||||
* @param successCallBack - function that should be triggered on every HTTP response with the status 2**.
|
||||
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
||||
*/
|
||||
public enableVerboseMode = (
|
||||
successCallBack = this.defaultInterceptionCallBack,
|
||||
errorCallBack = this.defaultInterceptionCallBack
|
||||
) => {
|
||||
this.httpInterceptor = this.httpClient.interceptors.response.use(
|
||||
successCallBack,
|
||||
errorCallBack
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off verbose mode to log every HTTP response.
|
||||
*/
|
||||
public disableVerboseMode = () => {
|
||||
if (this.httpInterceptor) {
|
||||
this.httpClient.interceptors.response.eject(this.httpInterceptor)
|
||||
}
|
||||
}
|
||||
|
||||
protected getHeaders = (
|
||||
accessToken: string | undefined,
|
||||
contentType: string
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { RequestClient } from './RequestClient'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { SasjsParsedResponse } from '../types'
|
||||
import { SASJS_LOGS_SEPARATOR } from '../utils'
|
||||
|
||||
interface SasjsParsedResponse<T> {
|
||||
result: T
|
||||
log: string
|
||||
etag: string
|
||||
status: number
|
||||
printOutput?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific request client for SASJS.
|
||||
* Append tokens in headers.
|
||||
*/
|
||||
|
||||
export class SasjsRequestClient extends RequestClient {
|
||||
getHeaders = (accessToken: string | undefined, contentType: string) => {
|
||||
const headers: any = {}
|
||||
@@ -36,30 +45,13 @@ export class SasjsRequestClient extends RequestClient {
|
||||
}
|
||||
} catch {
|
||||
if (response.data.includes(SASJS_LOGS_SEPARATOR)) {
|
||||
const { data } = response
|
||||
const splittedResponse: string[] = data.split(SASJS_LOGS_SEPARATOR)
|
||||
const splittedResponse = response.data.split(SASJS_LOGS_SEPARATOR)
|
||||
|
||||
webout = splittedResponse.splice(0, 1)[0]
|
||||
webout = splittedResponse[0]
|
||||
if (webout !== undefined) parsedResponse = webout
|
||||
|
||||
// log can contain nested logs
|
||||
const logs = splittedResponse.splice(0, splittedResponse.length - 1)
|
||||
|
||||
// tests if string ends with SASJS_LOGS_SEPARATOR
|
||||
const endingWithLogSepRegExp = new RegExp(`${SASJS_LOGS_SEPARATOR}$`)
|
||||
|
||||
// at this point splittedResponse can contain only one item
|
||||
const lastChunk = splittedResponse[0]
|
||||
|
||||
if (lastChunk) {
|
||||
// if the last chunk doesn't end with SASJS_LOGS_SEPARATOR, then it is a printOutput
|
||||
// else the last chunk is part of the log and has to be joined
|
||||
if (!endingWithLogSepRegExp.test(data)) printOutput = lastChunk
|
||||
else if (logs.length > 1) logs.push(lastChunk)
|
||||
}
|
||||
|
||||
// join logs into single log with SASJS_LOGS_SEPARATOR
|
||||
log = logs.join(SASJS_LOGS_SEPARATOR)
|
||||
log = splittedResponse[1]
|
||||
printOutput = splittedResponse[2]
|
||||
} else {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
@@ -67,7 +59,7 @@ export class SasjsRequestClient extends RequestClient {
|
||||
|
||||
const returnResult: SasjsParsedResponse<T> = {
|
||||
result: parsedResponse as T,
|
||||
log: log || '',
|
||||
log,
|
||||
etag,
|
||||
status: response.status
|
||||
}
|
||||
@@ -77,6 +69,3 @@ export class SasjsRequestClient extends RequestClient {
|
||||
return returnResult
|
||||
}
|
||||
}
|
||||
|
||||
export const SASJS_LOGS_SEPARATOR =
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { SASJS_LOGS_SEPARATOR, SasjsRequestClient } from '../SasjsRequestClient'
|
||||
import { SasjsParsedResponse } from '../../types'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
describe('SasjsRequestClient', () => {
|
||||
const requestClient = new SasjsRequestClient('')
|
||||
const etag = 'etag'
|
||||
const status = 200
|
||||
|
||||
const webout = `hello`
|
||||
const log = `1 The SAS System Tuesday, 25 July 2023 12:51:00
|
||||
|
||||
|
||||
PROC MIGRATE will preserve current SAS file attributes and is
|
||||
recommended for converting all your SAS libraries from any
|
||||
SAS 8 release to SAS 9. For details and examples, please see
|
||||
http://support.sas.com/rnd/migration/index.html
|
||||
|
||||
|
||||
|
||||
NOTE: SAS initialization used:
|
||||
real time 0.01 seconds
|
||||
cpu time 0.02 seconds
|
||||
|
||||
|
||||
`
|
||||
const printOutput = 'printOutPut'
|
||||
|
||||
describe('parseResponse', () => {})
|
||||
|
||||
it('should parse response with 1 log', () => {
|
||||
const response: AxiosResponse<any> = {
|
||||
data: `${webout}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${log}
|
||||
${SASJS_LOGS_SEPARATOR}`,
|
||||
status,
|
||||
statusText: 'ok',
|
||||
headers: { etag },
|
||||
config: {}
|
||||
}
|
||||
|
||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||
result: `${webout}
|
||||
`,
|
||||
log: `
|
||||
${log}
|
||||
`,
|
||||
etag,
|
||||
status
|
||||
}
|
||||
|
||||
expect(requestClient['parseResponse'](response)).toEqual(
|
||||
expectedParsedResponse
|
||||
)
|
||||
})
|
||||
|
||||
it('should parse response with 1 log and printOutput', () => {
|
||||
const response: AxiosResponse<any> = {
|
||||
data: `${webout}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${log}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${printOutput}`,
|
||||
status,
|
||||
statusText: 'ok',
|
||||
headers: { etag },
|
||||
config: {}
|
||||
}
|
||||
|
||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||
result: `${webout}
|
||||
`,
|
||||
log: `
|
||||
${log}
|
||||
`,
|
||||
etag,
|
||||
status,
|
||||
printOutput: `
|
||||
${printOutput}`
|
||||
}
|
||||
|
||||
expect(requestClient['parseResponse'](response)).toEqual(
|
||||
expectedParsedResponse
|
||||
)
|
||||
})
|
||||
|
||||
it('should parse response with nested logs', () => {
|
||||
const logWithNestedLog = `root log start
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${log}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
root log end`
|
||||
|
||||
const response: AxiosResponse<any> = {
|
||||
data: `${webout}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${logWithNestedLog}
|
||||
${SASJS_LOGS_SEPARATOR}`,
|
||||
status,
|
||||
statusText: 'ok',
|
||||
headers: { etag },
|
||||
config: {}
|
||||
}
|
||||
|
||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||
result: `${webout}
|
||||
`,
|
||||
log: `
|
||||
${logWithNestedLog}
|
||||
`,
|
||||
etag,
|
||||
status
|
||||
}
|
||||
|
||||
expect(requestClient['parseResponse'](response)).toEqual(
|
||||
expectedParsedResponse
|
||||
)
|
||||
})
|
||||
|
||||
it('should parse response with nested logs and printOutput', () => {
|
||||
const logWithNestedLog = `root log start
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${log}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
log with indentation
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${log}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
some SAS code containing ${SASJS_LOGS_SEPARATOR}
|
||||
root log end`
|
||||
|
||||
const response: AxiosResponse<any> = {
|
||||
data: `${webout}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${logWithNestedLog}
|
||||
${SASJS_LOGS_SEPARATOR}
|
||||
${printOutput}`,
|
||||
status,
|
||||
statusText: 'ok',
|
||||
headers: { etag },
|
||||
config: {}
|
||||
}
|
||||
|
||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
||||
result: `${webout}
|
||||
`,
|
||||
log: `
|
||||
${logWithNestedLog}
|
||||
`,
|
||||
etag,
|
||||
status,
|
||||
printOutput: `
|
||||
${printOutput}`
|
||||
}
|
||||
|
||||
expect(requestClient['parseResponse'](response)).toEqual(
|
||||
expectedParsedResponse
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SASJS_LOGS_SEPARATOR', () => {
|
||||
it('SASJS_LOGS_SEPARATOR should be hardcoded', () => {
|
||||
expect(SASJS_LOGS_SEPARATOR).toEqual(
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -5,19 +5,14 @@ import { app, mockedAuthResponse } from './SAS_server_app'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import SASjs from '../SASjs'
|
||||
import * as axiosModules from '../utils/createAxiosInstance'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
LoginRequiredError,
|
||||
AuthorizeError,
|
||||
NotFoundError,
|
||||
InternalServerError,
|
||||
VerboseMode
|
||||
} from '../types'
|
||||
InternalServerError
|
||||
} from '../types/errors'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
|
||||
import { AxiosResponse, AxiosError } from 'axios'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import * as UtilsModule from 'util'
|
||||
|
||||
const axiosActual = jest.requireActual('axios')
|
||||
|
||||
@@ -30,6 +25,16 @@ jest
|
||||
const PORT = 8000
|
||||
const SERVER_URL = `https://localhost:${PORT}/`
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
selfSigned: 'self signed certificate',
|
||||
CCA: 'unable to verify the first certificate'
|
||||
}
|
||||
|
||||
const incorrectAuthCodeErr = {
|
||||
error: 'unauthorized',
|
||||
error_description: 'Bad credentials'
|
||||
}
|
||||
|
||||
describe('RequestClient', () => {
|
||||
let server: http.Server
|
||||
|
||||
@@ -75,411 +80,6 @@ describe('RequestClient', () => {
|
||||
expect(rejectionErrorMessage).toEqual(expectedError.message)
|
||||
})
|
||||
|
||||
describe('defaultInterceptionCallBack', () => {
|
||||
const reqHeaders = `POST https://sas.server.com/compute/sessions/session_id/jobs HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
User-Agent: axios/0.27.2
|
||||
Content-Length: 334
|
||||
host: sas.server.io
|
||||
Connection: close
|
||||
`
|
||||
const reqData = `{
|
||||
name: 'test_job',
|
||||
description: 'Powered by SASjs',
|
||||
code: ['test_code'],
|
||||
variables: {
|
||||
SYS_JES_JOB_URI: '',
|
||||
_program: '/Public/sasjs/jobs/jobs/test_job'
|
||||
},
|
||||
arguments: {
|
||||
_contextName: 'SAS Job Execution compute context',
|
||||
_OMITJSONLISTING: true,
|
||||
_OMITJSONLOG: true,
|
||||
_OMITSESSIONRESULTS: true,
|
||||
_OMITTEXTLISTING: true,
|
||||
_OMITTEXTLOG: true
|
||||
}
|
||||
}`
|
||||
|
||||
const resHeaders = ['content-type', 'application/json']
|
||||
const resData = {
|
||||
id: 'id_string',
|
||||
name: 'name_string',
|
||||
uri: 'uri_string',
|
||||
createdBy: 'createdBy_string',
|
||||
code: 'TEST CODE',
|
||||
links: [
|
||||
{
|
||||
method: 'method_string',
|
||||
rel: 'state',
|
||||
href: 'state_href_string',
|
||||
uri: 'uri_string',
|
||||
type: 'type_string'
|
||||
},
|
||||
{
|
||||
method: 'method_string',
|
||||
rel: 'state',
|
||||
href: 'state_href_string',
|
||||
uri: 'uri_string',
|
||||
type: 'type_string'
|
||||
},
|
||||
{
|
||||
method: 'method_string',
|
||||
rel: 'state',
|
||||
href: 'state_href_string',
|
||||
uri: 'uri_string',
|
||||
type: 'type_string'
|
||||
},
|
||||
{
|
||||
method: 'method_string',
|
||||
rel: 'state',
|
||||
href: 'state_href_string',
|
||||
uri: 'uri_string',
|
||||
type: 'type_string'
|
||||
},
|
||||
{
|
||||
method: 'method_string',
|
||||
rel: 'state',
|
||||
href: 'state_href_string',
|
||||
uri: 'uri_string',
|
||||
type: 'type_string'
|
||||
},
|
||||
{
|
||||
method: 'method_string',
|
||||
rel: 'self',
|
||||
href: 'self_href_string',
|
||||
uri: 'uri_string',
|
||||
type: 'type_string'
|
||||
}
|
||||
],
|
||||
results: { '_webout.json': '_webout.json_string' },
|
||||
logStatistics: {
|
||||
lineCount: 1,
|
||||
modifiedTimeStamp: 'modifiedTimeStamp_string'
|
||||
}
|
||||
}
|
||||
beforeAll(() => {
|
||||
;(process as any).logger = new Logger(LogLevel.Off)
|
||||
jest.spyOn((process as any).logger, 'info')
|
||||
})
|
||||
|
||||
it('should log parsed response with status 1**', () => {
|
||||
const spyIsAxiosError = jest
|
||||
.spyOn(axios, 'isAxiosError')
|
||||
.mockImplementation(() => true)
|
||||
|
||||
const mockedAxiosError = {
|
||||
config: {
|
||||
data: reqData
|
||||
},
|
||||
request: {
|
||||
_currentRequest: {
|
||||
_header: reqHeaders
|
||||
}
|
||||
}
|
||||
} as AxiosError
|
||||
|
||||
const requestClient = new RequestClient('')
|
||||
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
||||
|
||||
const noValueMessage = 'Not provided'
|
||||
const expectedLog = `HTTP Request (first 50 lines):
|
||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||
|
||||
HTTP Response Code: ${requestClient['prettifyString'](noValueMessage)}
|
||||
|
||||
HTTP Response (first 50 lines):
|
||||
${noValueMessage}
|
||||
\n${requestClient['parseInterceptedBody'](noValueMessage)}
|
||||
`
|
||||
|
||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||
|
||||
spyIsAxiosError.mockReset()
|
||||
})
|
||||
|
||||
it('should log parsed response with status 2**', () => {
|
||||
const status = getRandomStatus([
|
||||
200, 201, 202, 203, 204, 205, 206, 207, 208, 226
|
||||
])
|
||||
|
||||
const mockedResponse: AxiosResponse = {
|
||||
data: resData,
|
||||
status,
|
||||
statusText: '',
|
||||
headers: {},
|
||||
config: { data: reqData },
|
||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||
}
|
||||
|
||||
const requestClient = new RequestClient('')
|
||||
requestClient['defaultInterceptionCallBack'](mockedResponse)
|
||||
|
||||
const expectedLog = `HTTP Request (first 50 lines):
|
||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||
|
||||
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
||||
|
||||
HTTP Response (first 50 lines):
|
||||
${resHeaders[0]}: ${resHeaders[1]}${
|
||||
requestClient['parseInterceptedBody'](resData)
|
||||
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
|
||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||
})
|
||||
|
||||
it('should log parsed response with status 3**', () => {
|
||||
const status = getRandomStatus([300, 301, 302, 303, 304, 307, 308])
|
||||
|
||||
const mockedResponse: AxiosResponse = {
|
||||
data: resData,
|
||||
status,
|
||||
statusText: '',
|
||||
headers: {},
|
||||
config: { data: reqData },
|
||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||
}
|
||||
|
||||
const requestClient = new RequestClient('')
|
||||
requestClient['defaultInterceptionCallBack'](mockedResponse)
|
||||
|
||||
const expectedLog = `HTTP Request (first 50 lines):
|
||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||
|
||||
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
||||
|
||||
HTTP Response (first 50 lines):
|
||||
${resHeaders[0]}: ${resHeaders[1]}${
|
||||
requestClient['parseInterceptedBody'](resData)
|
||||
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
|
||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||
})
|
||||
|
||||
it('should log parsed response with status 4**', () => {
|
||||
const spyIsAxiosError = jest
|
||||
.spyOn(axios, 'isAxiosError')
|
||||
.mockImplementation(() => true)
|
||||
|
||||
const status = getRandomStatus([
|
||||
400, 401, 402, 403, 404, 407, 408, 409, 410, 411, 412, 413, 414, 415,
|
||||
416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451
|
||||
])
|
||||
|
||||
const mockedResponse: AxiosResponse = {
|
||||
data: resData,
|
||||
status,
|
||||
statusText: '',
|
||||
headers: {},
|
||||
config: { data: reqData },
|
||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||
}
|
||||
const mockedAxiosError = {
|
||||
config: {
|
||||
data: reqData
|
||||
},
|
||||
request: {
|
||||
_currentRequest: {
|
||||
_header: reqHeaders
|
||||
}
|
||||
},
|
||||
response: mockedResponse
|
||||
} as AxiosError
|
||||
|
||||
const requestClient = new RequestClient('')
|
||||
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
||||
|
||||
const expectedLog = `HTTP Request (first 50 lines):
|
||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||
|
||||
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
||||
|
||||
HTTP Response (first 50 lines):
|
||||
${resHeaders[0]}: ${resHeaders[1]}${
|
||||
requestClient['parseInterceptedBody'](resData)
|
||||
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
|
||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||
|
||||
spyIsAxiosError.mockReset()
|
||||
})
|
||||
|
||||
it('should log parsed response with status 5**', () => {
|
||||
const spyIsAxiosError = jest
|
||||
.spyOn(axios, 'isAxiosError')
|
||||
.mockImplementation(() => true)
|
||||
|
||||
const status = getRandomStatus([
|
||||
500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511
|
||||
])
|
||||
|
||||
const mockedResponse: AxiosResponse = {
|
||||
data: resData,
|
||||
status,
|
||||
statusText: '',
|
||||
headers: {},
|
||||
config: { data: reqData },
|
||||
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||
}
|
||||
const mockedAxiosError = {
|
||||
config: {
|
||||
data: reqData
|
||||
},
|
||||
request: {
|
||||
_currentRequest: {
|
||||
_header: reqHeaders
|
||||
}
|
||||
},
|
||||
response: mockedResponse
|
||||
} as AxiosError
|
||||
|
||||
const requestClient = new RequestClient('')
|
||||
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
||||
|
||||
const expectedLog = `HTTP Request (first 50 lines):
|
||||
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||
|
||||
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
||||
|
||||
HTTP Response (first 50 lines):
|
||||
${resHeaders[0]}: ${resHeaders[1]}${
|
||||
requestClient['parseInterceptedBody'](resData)
|
||||
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
|
||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||
|
||||
spyIsAxiosError.mockReset()
|
||||
})
|
||||
})
|
||||
|
||||
describe('enableVerboseMode', () => {
|
||||
it('should add defaultInterceptionCallBack functions to response interceptors', () => {
|
||||
const requestClient = new RequestClient('')
|
||||
const interceptorSpy = jest.spyOn(
|
||||
requestClient['httpClient'].interceptors.response,
|
||||
'use'
|
||||
)
|
||||
|
||||
requestClient.enableVerboseMode()
|
||||
|
||||
expect(interceptorSpy).toHaveBeenCalledWith(
|
||||
requestClient['defaultInterceptionCallBack'],
|
||||
requestClient['defaultInterceptionCallBack']
|
||||
)
|
||||
})
|
||||
|
||||
it('should add callback functions to response interceptors', () => {
|
||||
const requestClient = new RequestClient('')
|
||||
const interceptorSpy = jest.spyOn(
|
||||
requestClient['httpClient'].interceptors.response,
|
||||
'use'
|
||||
)
|
||||
|
||||
const successCallback = (response: AxiosResponse | AxiosError) => {
|
||||
console.log('success')
|
||||
|
||||
return response
|
||||
}
|
||||
const failureCallback = (response: AxiosResponse | AxiosError) => {
|
||||
console.log('failure')
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
requestClient.enableVerboseMode(successCallback, failureCallback)
|
||||
|
||||
expect(interceptorSpy).toHaveBeenCalledWith(
|
||||
successCallback,
|
||||
failureCallback
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setVerboseMode', () => {
|
||||
it(`should set verbose mode`, () => {
|
||||
const requestClient = new RequestClient('')
|
||||
let verbose: VerboseMode = false
|
||||
requestClient.setVerboseMode(verbose)
|
||||
|
||||
expect(requestClient['verboseMode']).toEqual(verbose)
|
||||
|
||||
verbose = true
|
||||
requestClient.setVerboseMode(verbose)
|
||||
|
||||
expect(requestClient['verboseMode']).toEqual(verbose)
|
||||
|
||||
verbose = 'bleached'
|
||||
requestClient.setVerboseMode(verbose)
|
||||
|
||||
expect(requestClient['verboseMode']).toEqual(verbose)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prettifyString', () => {
|
||||
it(`should call inspect without colors when verbose mode is set to 'bleached'`, () => {
|
||||
const requestClient = new RequestClient('')
|
||||
let verbose: VerboseMode = 'bleached'
|
||||
requestClient.setVerboseMode(verbose)
|
||||
|
||||
jest.spyOn(UtilsModule, 'inspect')
|
||||
|
||||
const testStr = JSON.stringify({ test: 'test' })
|
||||
|
||||
requestClient['prettifyString'](testStr)
|
||||
|
||||
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
||||
colors: false
|
||||
})
|
||||
})
|
||||
|
||||
it(`should call inspect with colors when verbose mode is set to 'true'`, () => {
|
||||
const requestClient = new RequestClient('')
|
||||
let verbose: VerboseMode = true
|
||||
requestClient.setVerboseMode(verbose)
|
||||
|
||||
jest.spyOn(UtilsModule, 'inspect')
|
||||
|
||||
const testStr = JSON.stringify({ test: 'test' })
|
||||
|
||||
requestClient['prettifyString'](testStr)
|
||||
|
||||
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
||||
colors: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('disableVerboseMode', () => {
|
||||
it('should eject interceptor', () => {
|
||||
const requestClient = new RequestClient('')
|
||||
|
||||
const interceptorSpy = jest.spyOn(
|
||||
requestClient['httpClient'].interceptors.response,
|
||||
'eject'
|
||||
)
|
||||
|
||||
const interceptorId = 100
|
||||
|
||||
requestClient['httpInterceptor'] = interceptorId
|
||||
requestClient.disableVerboseMode()
|
||||
|
||||
expect(interceptorSpy).toHaveBeenCalledWith(interceptorId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleError', () => {
|
||||
const requestClient = new RequestClient('https://localhost:8009')
|
||||
const randomError = 'some error'
|
||||
@@ -693,11 +293,3 @@ const createCertificate = async (): Promise<pem.CertificateCreationResult> => {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random status code.
|
||||
* @param statuses - an array of available statuses.
|
||||
* @returns - random item from an array of statuses.
|
||||
*/
|
||||
const getRandomStatus = (statuses: number[]) =>
|
||||
statuses[Math.floor(Math.random() * statuses.length)]
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RequestClient } from '../request/RequestClient'
|
||||
import * as dotenv from 'dotenv'
|
||||
import axios from 'axios'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { Session, SessionState, Context } from '../types'
|
||||
import { Session, Context } from '../types'
|
||||
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
@@ -11,34 +11,21 @@ const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
|
||||
describe('SessionManager', () => {
|
||||
dotenv.config()
|
||||
process.env.SERVER_URL = 'https://server.com'
|
||||
|
||||
const sessionManager = new SessionManager(
|
||||
process.env.SERVER_URL as string,
|
||||
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
||||
requestClient
|
||||
)
|
||||
const sessionStateLink = '/compute/sessions/session-id-ses0000/state'
|
||||
const sessionEtag = 'etag-string'
|
||||
|
||||
const getMockSession = (): Session => ({
|
||||
const getMockSession = () => ({
|
||||
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
||||
state: SessionState.NoState,
|
||||
links: [
|
||||
{
|
||||
href: sessionStateLink,
|
||||
method: 'GET',
|
||||
rel: 'state',
|
||||
type: 'text/plain',
|
||||
uri: sessionStateLink
|
||||
}
|
||||
],
|
||||
state: '',
|
||||
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
||||
attributes: {
|
||||
sessionInactiveTimeout: 900
|
||||
},
|
||||
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`,
|
||||
stateUrl: sessionStateLink,
|
||||
etag: sessionEtag
|
||||
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -102,21 +89,19 @@ describe('SessionManager', () => {
|
||||
describe('waitForSession', () => {
|
||||
const session: Session = {
|
||||
id: 'id',
|
||||
state: SessionState.NoState,
|
||||
state: '',
|
||||
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
||||
attributes: {
|
||||
sessionInactiveTimeout: 0
|
||||
},
|
||||
creationTimeStamp: '',
|
||||
stateUrl: sessionStateLink,
|
||||
etag: sessionEtag
|
||||
creationTimeStamp: ''
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
;(process as any).logger = new Logger(LogLevel.Off)
|
||||
})
|
||||
|
||||
it('should log http response code and session state if SAS server did not provide session state', async () => {
|
||||
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
|
||||
let requestAttempt = 0
|
||||
const requestAttemptLimit = 10
|
||||
const sessionState = 'idle'
|
||||
@@ -139,17 +124,15 @@ describe('SessionManager', () => {
|
||||
sessionManager['waitForSession'](session, null, 'access_token')
|
||||
).resolves.toEqual(sessionState)
|
||||
|
||||
const sessionStateUrl = process.env.SERVER_URL + session.stateUrl
|
||||
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit)
|
||||
expect((process as any).logger.info).toHaveBeenCalledTimes(3)
|
||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`Polling: ${sessionStateUrl}`
|
||||
`Polling: ${process.env.SERVER_URL}`
|
||||
)
|
||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
`Could not get session state. Server responded with 304 whilst checking state: ${sessionStateUrl}`
|
||||
`Could not get session state. Server responded with 304 whilst checking state: ${process.env.SERVER_URL}`
|
||||
)
|
||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
@@ -159,7 +142,7 @@ describe('SessionManager', () => {
|
||||
|
||||
it('should throw an error if there is no session link', async () => {
|
||||
const customSession = JSON.parse(JSON.stringify(session))
|
||||
customSession.stateUrl = ''
|
||||
customSession.links = []
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: customSession.state, status: 200 })
|
||||
@@ -173,7 +156,6 @@ describe('SessionManager', () => {
|
||||
it('should throw an error if could not get session state', async () => {
|
||||
const gettingSessionStatus = 500
|
||||
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(() =>
|
||||
Promise.reject({
|
||||
@@ -186,7 +168,7 @@ describe('SessionManager', () => {
|
||||
})
|
||||
)
|
||||
|
||||
const expectedError = `Error while waiting for session. Error while getting session state. GET request to ${sessionStateUrl}?wait=30 failed with status code ${gettingSessionStatus}. ${sessionStatusError}`
|
||||
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}`
|
||||
|
||||
await expect(
|
||||
sessionManager['waitForSession'](session, null, 'access_token')
|
||||
@@ -445,45 +427,4 @@ 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,55 +0,0 @@
|
||||
import { CsrfToken } from '..'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
put<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
delete<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
|
||||
saveLocalStorageToken(accessToken: string, refreshToken: string): void
|
||||
clearCsrfTokens(): void
|
||||
clearLocalStorageTokens(): void
|
||||
getBaseUrl(): string
|
||||
}
|
||||
|
||||
export interface SASjsRequest {
|
||||
serviceLink: string
|
||||
timestamp: Date
|
||||
sourceCode: string
|
||||
generatedCode: string
|
||||
logFile: string
|
||||
SASWORK: any
|
||||
}
|
||||
|
||||
export interface SasjsParsedResponse<T> {
|
||||
result: T
|
||||
log: string
|
||||
etag: string
|
||||
status: number
|
||||
printOutput?: string
|
||||
}
|
||||
|
||||
export type VerboseMode = boolean | 'bleached'
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as https from 'https'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { VerboseMode } from '../types'
|
||||
|
||||
/**
|
||||
* Specifies the configuration for the SASjs instance - eg where and how to
|
||||
@@ -46,10 +45,6 @@ export class SASjsConfig {
|
||||
* Set to `true` to enable additional debugging.
|
||||
*/
|
||||
debug: boolean = true
|
||||
/**
|
||||
* Set to `true` to enable verbose mode that will log a summary of every HTTP response.
|
||||
*/
|
||||
verbose?: VerboseMode = true
|
||||
/**
|
||||
* The name of the compute context to use when calling the Viya services directly.
|
||||
* Example value: 'SAS Job Execution compute context'
|
||||
|
||||
12
src/types/SASjsRequest.ts
Normal file
12
src/types/SASjsRequest.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Represents a SASjs request, its response and logs.
|
||||
*
|
||||
*/
|
||||
export interface SASjsRequest {
|
||||
serviceLink: string
|
||||
timestamp: Date
|
||||
sourceCode: string
|
||||
generatedCode: string
|
||||
logFile: string
|
||||
SASWORK: any
|
||||
}
|
||||
@@ -1,34 +1,15 @@
|
||||
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 {
|
||||
id: string
|
||||
state: SessionState
|
||||
stateUrl: string
|
||||
state: string
|
||||
links: Link[]
|
||||
attributes: {
|
||||
sessionInactiveTimeout: number
|
||||
}
|
||||
creationTimeStamp: string
|
||||
etag: string
|
||||
}
|
||||
|
||||
export interface SessionVariable {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface JobSessionManager {
|
||||
session: Session
|
||||
sessionManager: SessionManager
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ export * from './Job'
|
||||
export * from './JobDefinition'
|
||||
export * from './JobResult'
|
||||
export * from './Link'
|
||||
export * from './Login'
|
||||
export * from './SASjsConfig'
|
||||
export * from './RequestClient'
|
||||
export * from './SASjsRequest'
|
||||
export * from './Session'
|
||||
export * from './UploadFile'
|
||||
export * from './PollOptions'
|
||||
export * from './WriteStream'
|
||||
export * from './ExecuteScript'
|
||||
export * from './errors'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SASjsRequest } from '../types'
|
||||
import { SASjsRequest } from '../types/SASjsRequest'
|
||||
|
||||
/**
|
||||
* Comparator for SASjs request timestamps.
|
||||
|
||||
2
src/utils/constants.ts
Normal file
2
src/utils/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const SASJS_LOGS_SEPARATOR =
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
@@ -2,6 +2,7 @@ export * from './appendExtraResponseAttributes'
|
||||
export * from './asyncForEach'
|
||||
export * from './compareTimestamps'
|
||||
export * from './convertToCsv'
|
||||
export * from './constants'
|
||||
export * from './createAxiosInstance'
|
||||
export * from './delay'
|
||||
export * from './fetchLogByChunks'
|
||||
|
||||
Reference in New Issue
Block a user