mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-05 03:30:05 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3556eb3903 | |||
| 600e561a45 | |||
| 6a161a05ef | |||
| 8db02012e5 | |||
| a01b1a9feb | |||
| e6ec51c7eb |
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.
|
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`).
|
- [ ] 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)).
|
- (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
|
- [ ] [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: actions/checkout@v2
|
||||||
- uses: uesteibar/reviewer-lottery@v1
|
- uses: uesteibar/reviewer-lottery@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -80,10 +80,6 @@ jobs:
|
|||||||
npm run update:adapter
|
npm run update:adapter
|
||||||
pm2 start --name sasjs-test npm -- start
|
pm2 start --name sasjs-test npm -- start
|
||||||
|
|
||||||
- name: Sleep for 10 seconds
|
|
||||||
run: sleep 10s
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Run cypress on sasjs
|
- name: Run cypress on sasjs
|
||||||
run: |
|
run: |
|
||||||
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
|
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
|
- name: Push generated docs
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GH_TOKEN }}
|
||||||
publish_branch: gh-pages
|
publish_branch: gh-pages
|
||||||
publish_dir: ./docs
|
publish_dir: ./docs
|
||||||
cname: adapter.sasjs.io
|
cname: adapter.sasjs.io
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/npmpublish.yml
vendored
2
.github/workflows/npmpublish.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Semantic Release
|
- name: Semantic Release
|
||||||
uses: cycjimmy/semantic-release-action@v3
|
uses: cycjimmy/semantic-release-action@v3
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Send Matrix message
|
- 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 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.
|
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.
|
|
||||||
|
|
||||||
### Session Manager
|
### 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).
|
* `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.
|
* `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.
|
* `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.
|
* `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.
|
* `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`.
|
* `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
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
coverageThreshold: {
|
// coverageThreshold: undefined,
|
||||||
global: {
|
|
||||||
statements: 63.66,
|
|
||||||
branches: 44.74,
|
|
||||||
functions: 53.94,
|
|
||||||
lines: 64.12
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// A path to a custom dependency extractor
|
// A path to a custom dependency extractor
|
||||||
// dependencyExtractor: undefined,
|
// dependencyExtractor: undefined,
|
||||||
|
|||||||
66
src/SASjs.ts
66
src/SASjs.ts
@@ -4,12 +4,7 @@ import {
|
|||||||
UploadFile,
|
UploadFile,
|
||||||
EditContextInput,
|
EditContextInput,
|
||||||
PollOptions,
|
PollOptions,
|
||||||
LoginMechanism,
|
LoginMechanism
|
||||||
VerboseMode,
|
|
||||||
ErrorResponse,
|
|
||||||
LoginOptions,
|
|
||||||
LoginResult,
|
|
||||||
ExecutionQuery
|
|
||||||
} from './types'
|
} from './types'
|
||||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||||
@@ -34,7 +29,8 @@ import {
|
|||||||
Sas9JobExecutor,
|
Sas9JobExecutor,
|
||||||
FileUploader
|
FileUploader
|
||||||
} from './job-execution'
|
} from './job-execution'
|
||||||
import { AxiosResponse, AxiosError } from 'axios'
|
import { ErrorResponse } from './types/errors'
|
||||||
|
import { LoginOptions, LoginResult } from './types/Login'
|
||||||
|
|
||||||
interface ExecuteScriptParams {
|
interface ExecuteScriptParams {
|
||||||
linesOfCode: string[]
|
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.
|
* Gets compute contexts.
|
||||||
* @param accessToken - an access token for an authorised user.
|
* @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 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 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.
|
||||||
* @param verboseMode - boolean or a string equal to 'bleached' to enable verbose mode (log every HTTP response).
|
|
||||||
*/
|
*/
|
||||||
public async startComputeJob(
|
public async startComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
@@ -885,8 +863,7 @@ export default class SASjs {
|
|||||||
waitForResult?: boolean,
|
waitForResult?: boolean,
|
||||||
pollOptions?: PollOptions,
|
pollOptions?: PollOptions,
|
||||||
printPid = false,
|
printPid = false,
|
||||||
variables?: MacroVar,
|
variables?: MacroVar
|
||||||
verboseMode?: VerboseMode
|
|
||||||
) {
|
) {
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...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(
|
return this.sasViyaApiClient?.executeComputeJob(
|
||||||
sasJob,
|
sasJob,
|
||||||
config.contextName,
|
config.contextName,
|
||||||
@@ -998,8 +970,7 @@ export default class SASjs {
|
|||||||
this.requestClient = new RequestClientClass(
|
this.requestClient = new RequestClientClass(
|
||||||
this.sasjsConfig.serverUrl,
|
this.sasjsConfig.serverUrl,
|
||||||
this.sasjsConfig.httpsAgentOptions,
|
this.sasjsConfig.httpsAgentOptions,
|
||||||
this.sasjsConfig.requestHistoryLimit,
|
this.sasjsConfig.requestHistoryLimit
|
||||||
this.sasjsConfig.verbose
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.requestClient.setConfig(
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export async function executeScript(
|
|||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Triggering '${relativeJobPath}' with PID ${
|
`Triggered '${relativeJobPath}' with PID ${
|
||||||
jobIdVariable.value
|
jobIdVariable.value
|
||||||
} at ${timestampToYYYYMMDDHHMMSS()}`
|
} at ${timestampToYYYYMMDDHHMMSS()}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -276,76 +276,6 @@ describe('pollJobState', () => {
|
|||||||
expect(delays).toEqual([pollIntervals[0], ...pollIntervals])
|
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 () => {
|
it('should throw an error if not valid poll strategies provided', async () => {
|
||||||
// INFO: 'maxPollCount' has to be > 0
|
// INFO: 'maxPollCount' has to be > 0
|
||||||
let invalidPollStrategy = {
|
let invalidPollStrategy = {
|
||||||
|
|||||||
@@ -78,7 +78,16 @@ export class AuthManager {
|
|||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
if (this.serverType === ServerType.Sas9) {
|
if (this.serverType === ServerType.Sas9) {
|
||||||
await this.performCASSecurityCheck()
|
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||||
|
|
||||||
|
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||||
|
return {
|
||||||
|
isLoggedIn: false,
|
||||||
|
userName: this.userName || '',
|
||||||
|
userLongName: this.userLongName || '',
|
||||||
|
errorMessage: 'Public access has been denied.'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userName, userLongName } = await this.fetchUserName()
|
const { userName, userLongName } = await this.fetchUserName()
|
||||||
@@ -149,7 +158,17 @@ export class AuthManager {
|
|||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
if (this.serverType === ServerType.Sas9) {
|
if (this.serverType === ServerType.Sas9) {
|
||||||
await this.performCASSecurityCheck()
|
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||||
|
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||||
|
isLoggedIn = false
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoggedIn,
|
||||||
|
userName: this.userName || '',
|
||||||
|
userLongName: this.userLongName || '',
|
||||||
|
errorMessage: 'Public access has been denied.'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loginCallback()
|
this.loginCallback()
|
||||||
@@ -166,11 +185,15 @@ export class AuthManager {
|
|||||||
private async performCASSecurityCheck() {
|
private async performCASSecurityCheck() {
|
||||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||||
|
|
||||||
await this.requestClient
|
return await this.requestClient
|
||||||
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// ignore if resource not found error
|
// ignore if resource not found error
|
||||||
if (!(err instanceof NotFoundError)) throw err
|
if (!(err instanceof NotFoundError)) throw err
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: ''
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,3 +410,7 @@ const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
|
|||||||
|
|
||||||
return /You have signed in/gm.test(response)
|
return /You have signed in/gm.test(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPublicAccessDenied = (response: any): boolean => {
|
||||||
|
return /Public access has been denied/gm.test(response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import axios from 'axios'
|
|||||||
import {
|
import {
|
||||||
mockedCurrentUserApi,
|
mockedCurrentUserApi,
|
||||||
mockLoginAuthoriseRequiredResponse,
|
mockLoginAuthoriseRequiredResponse,
|
||||||
|
mockLoginPublicAccessDeniedResponse,
|
||||||
mockLoginSuccessResponse
|
mockLoginSuccessResponse
|
||||||
} from './mockResponses'
|
} from './mockResponses'
|
||||||
import { serialize } from '../../utils'
|
import { serialize } from '../../utils'
|
||||||
@@ -213,6 +214,61 @@ describe('AuthManager', () => {
|
|||||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should post a login & a cas_security request to the SAS9 server when not logged in & get rejected due to public access denied', async () => {
|
||||||
|
const serverType = ServerType.Sas9
|
||||||
|
const authManager = new AuthManager(
|
||||||
|
serverUrl,
|
||||||
|
serverType,
|
||||||
|
requestClient,
|
||||||
|
authCallback
|
||||||
|
)
|
||||||
|
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
isLoggedIn: false,
|
||||||
|
userName: '',
|
||||||
|
userLongName: '',
|
||||||
|
loginForm: { name: 'test' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
mockedAxios.post.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||||
|
)
|
||||||
|
mockedAxios.get.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginResponse = await authManager.logIn(userName, password)
|
||||||
|
|
||||||
|
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||||
|
expect(loginResponse.userName).toEqual('')
|
||||||
|
expect(loginResponse.errorMessage).toEqual(
|
||||||
|
'Public access has been denied.'
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginParams = serialize({
|
||||||
|
_service: 'default',
|
||||||
|
username: userName,
|
||||||
|
password,
|
||||||
|
name: 'test'
|
||||||
|
})
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||||
|
`/SASLogon/login`,
|
||||||
|
loginParams,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: '*/*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const casAuthenticationUrl = `${serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||||
|
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||||
|
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||||
|
getHeadersJson
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should return empty username if unable to logged in', async () => {
|
it('should return empty username if unable to logged in', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
@@ -422,6 +478,53 @@ describe('AuthManager', () => {
|
|||||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should return error if public account access is denied', async () => {
|
||||||
|
const serverType = ServerType.Sas9
|
||||||
|
const authManager = new AuthManager(
|
||||||
|
serverUrl,
|
||||||
|
serverType,
|
||||||
|
requestClient,
|
||||||
|
authCallback
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
isLoggedIn: false,
|
||||||
|
userName: ''
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
isLoggedIn: true,
|
||||||
|
userName
|
||||||
|
})
|
||||||
|
)
|
||||||
|
mockedAxios.get.mockImplementation(() =>
|
||||||
|
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginResponse = await authManager.redirectedLogIn({})
|
||||||
|
|
||||||
|
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||||
|
expect(loginResponse.userName).toEqual('')
|
||||||
|
expect(loginResponse.errorMessage).toEqual(
|
||||||
|
'Public access has been denied.'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||||
|
`/SASLogon`,
|
||||||
|
'SASLogon',
|
||||||
|
{
|
||||||
|
width: 500,
|
||||||
|
height: 600
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
|
||||||
|
expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should return empty username if user unable to re-login via pop up', async () => {
|
it('should return empty username if user unable to re-login via pop up', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { SasAuthResponse } from '@sasjs/utils/types'
|
|||||||
|
|
||||||
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
export const 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 mockLoginSuccessResponse = `You have signed in`
|
||||||
|
export const mockLoginPublicAccessDeniedResponse = `Public access has been denied`
|
||||||
|
|
||||||
export const mockAuthResponse: SasAuthResponse = {
|
export const mockAuthResponse: SasAuthResponse = {
|
||||||
access_token: 'acc355',
|
access_token: 'acc355',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
getValidJson,
|
getValidJson,
|
||||||
parseSasViyaDebugResponse,
|
parseSasViyaDebugResponse,
|
||||||
parseWeboutResponse
|
parseWeboutResponse,
|
||||||
|
SASJS_LOGS_SEPARATOR
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { UploadFile } from '../types/UploadFile'
|
import { UploadFile } from '../types/UploadFile'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -187,6 +187,12 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
{ result: jsonResponse, log: res.log },
|
{ result: jsonResponse, log: res.log },
|
||||||
extraResponseAttributes
|
extraResponseAttributes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (this.isPublicAccessDenied(jsonResponse))
|
||||||
|
reject(
|
||||||
|
new ErrorResponse('Public access has been denied', responseObject)
|
||||||
|
)
|
||||||
|
|
||||||
resolve(responseObject)
|
resolve(responseObject)
|
||||||
})
|
})
|
||||||
.catch(async (e: Error) => {
|
.catch(async (e: Error) => {
|
||||||
@@ -262,4 +268,8 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
}
|
}
|
||||||
return uri
|
return uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isPublicAccessDenied = (response: string): boolean => {
|
||||||
|
return /Public access has been denied/gm.test(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,8 +233,7 @@ export default class SASjs {
|
|||||||
this.requestClient = new RequestClient(
|
this.requestClient = new RequestClient(
|
||||||
this.sasjsConfig.serverUrl,
|
this.sasjsConfig.serverUrl,
|
||||||
this.sasjsConfig.httpsAgentOptions,
|
this.sasjsConfig.httpsAgentOptions,
|
||||||
this.sasjsConfig.requestHistoryLimit,
|
this.sasjsConfig.requestHistoryLimit
|
||||||
this.sasjsConfig.verbose
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.requestClient.setConfig(
|
this.requestClient.setConfig(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import {
|
import {
|
||||||
isRelativePath,
|
isRelativePath,
|
||||||
|
parseSasViyaDebugResponse,
|
||||||
appendExtraResponseAttributes,
|
appendExtraResponseAttributes,
|
||||||
convertToCSV
|
convertToCSV
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
AxiosError,
|
|
||||||
AxiosInstance,
|
|
||||||
AxiosRequestConfig,
|
|
||||||
AxiosResponse
|
|
||||||
} from 'axios'
|
|
||||||
import axios from 'axios'
|
|
||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { CsrfToken } from '..'
|
import { CsrfToken } from '..'
|
||||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||||
@@ -16,7 +10,7 @@ import {
|
|||||||
JobExecutionError,
|
JobExecutionError,
|
||||||
CertificateError
|
CertificateError
|
||||||
} from '../types/errors'
|
} from '../types/errors'
|
||||||
import { SASjsRequest, HttpClient, VerboseMode } from '../types'
|
import { SASjsRequest } from '../types'
|
||||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
||||||
@@ -26,13 +20,45 @@ import {
|
|||||||
createAxiosInstance
|
createAxiosInstance
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
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 {
|
export class RequestClient implements HttpClient {
|
||||||
private requests: SASjsRequest[] = []
|
private requests: SASjsRequest[] = []
|
||||||
private requestsLimit: number = 10
|
private requestsLimit: number = 10
|
||||||
private httpInterceptor?: number
|
|
||||||
private verboseMode: VerboseMode = false
|
|
||||||
|
|
||||||
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||||
protected fileUploadCsrfToken: CsrfToken | undefined
|
protected fileUploadCsrfToken: CsrfToken | undefined
|
||||||
@@ -41,17 +67,10 @@ export class RequestClient implements HttpClient {
|
|||||||
constructor(
|
constructor(
|
||||||
protected baseUrl: string,
|
protected baseUrl: string,
|
||||||
httpsAgentOptions?: https.AgentOptions,
|
httpsAgentOptions?: https.AgentOptions,
|
||||||
requestsLimit?: number,
|
requestsLimit?: number
|
||||||
verboseMode?: VerboseMode
|
|
||||||
) {
|
) {
|
||||||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||||||
|
|
||||||
if (requestsLimit) this.requestsLimit = requestsLimit
|
if (requestsLimit) this.requestsLimit = requestsLimit
|
||||||
|
|
||||||
if (verboseMode) {
|
|
||||||
this.setVerboseMode(verboseMode)
|
|
||||||
this.enableVerboseMode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
||||||
@@ -71,7 +90,6 @@ export class RequestClient implements HttpClient {
|
|||||||
this.csrfToken = { headerName: '', value: '' }
|
this.csrfToken = { headerName: '', value: '' }
|
||||||
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearLocalStorageTokens() {
|
public clearLocalStorageTokens() {
|
||||||
localStorage.setItem('accessToken', '')
|
localStorage.setItem('accessToken', '')
|
||||||
localStorage.setItem('refreshToken', '')
|
localStorage.setItem('refreshToken', '')
|
||||||
@@ -162,7 +180,6 @@ export class RequestClient implements HttpClient {
|
|||||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentType === 'text/plain') {
|
if (contentType === 'text/plain') {
|
||||||
requestConfig.transformResponse = undefined
|
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 = (
|
protected getHeaders = (
|
||||||
accessToken: string | undefined,
|
accessToken: string | undefined,
|
||||||
contentType: string
|
contentType: string
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { RequestClient } from './RequestClient'
|
import { RequestClient } from './RequestClient'
|
||||||
import { AxiosResponse } from 'axios'
|
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.
|
* Specific request client for SASJS.
|
||||||
* Append tokens in headers.
|
* Append tokens in headers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class SasjsRequestClient extends RequestClient {
|
export class SasjsRequestClient extends RequestClient {
|
||||||
getHeaders = (accessToken: string | undefined, contentType: string) => {
|
getHeaders = (accessToken: string | undefined, contentType: string) => {
|
||||||
const headers: any = {}
|
const headers: any = {}
|
||||||
@@ -36,30 +45,13 @@ export class SasjsRequestClient extends RequestClient {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (response.data.includes(SASJS_LOGS_SEPARATOR)) {
|
if (response.data.includes(SASJS_LOGS_SEPARATOR)) {
|
||||||
const { data } = response
|
const splittedResponse = response.data.split(SASJS_LOGS_SEPARATOR)
|
||||||
const splittedResponse: string[] = data.split(SASJS_LOGS_SEPARATOR)
|
|
||||||
|
|
||||||
webout = splittedResponse.splice(0, 1)[0]
|
webout = splittedResponse[0]
|
||||||
if (webout !== undefined) parsedResponse = webout
|
if (webout !== undefined) parsedResponse = webout
|
||||||
|
|
||||||
// log can contain nested logs
|
log = splittedResponse[1]
|
||||||
const logs = splittedResponse.splice(0, splittedResponse.length - 1)
|
printOutput = splittedResponse[2]
|
||||||
|
|
||||||
// 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)
|
|
||||||
} else {
|
} else {
|
||||||
parsedResponse = response.data
|
parsedResponse = response.data
|
||||||
}
|
}
|
||||||
@@ -67,7 +59,7 @@ export class SasjsRequestClient extends RequestClient {
|
|||||||
|
|
||||||
const returnResult: SasjsParsedResponse<T> = {
|
const returnResult: SasjsParsedResponse<T> = {
|
||||||
result: parsedResponse as T,
|
result: parsedResponse as T,
|
||||||
log: log || '',
|
log,
|
||||||
etag,
|
etag,
|
||||||
status: response.status
|
status: response.status
|
||||||
}
|
}
|
||||||
@@ -77,6 +69,3 @@ export class SasjsRequestClient extends RequestClient {
|
|||||||
return returnResult
|
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 { ServerType } from '@sasjs/utils/types'
|
||||||
import SASjs from '../SASjs'
|
import SASjs from '../SASjs'
|
||||||
import * as axiosModules from '../utils/createAxiosInstance'
|
import * as axiosModules from '../utils/createAxiosInstance'
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
import {
|
||||||
LoginRequiredError,
|
LoginRequiredError,
|
||||||
AuthorizeError,
|
AuthorizeError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
InternalServerError,
|
InternalServerError
|
||||||
VerboseMode
|
} from '../types/errors'
|
||||||
} from '../types'
|
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
|
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')
|
const axiosActual = jest.requireActual('axios')
|
||||||
|
|
||||||
@@ -30,6 +25,16 @@ jest
|
|||||||
const PORT = 8000
|
const PORT = 8000
|
||||||
const SERVER_URL = `https://localhost:${PORT}/`
|
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', () => {
|
describe('RequestClient', () => {
|
||||||
let server: http.Server
|
let server: http.Server
|
||||||
|
|
||||||
@@ -75,411 +80,6 @@ describe('RequestClient', () => {
|
|||||||
expect(rejectionErrorMessage).toEqual(expectedError.message)
|
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', () => {
|
describe('handleError', () => {
|
||||||
const requestClient = new RequestClient('https://localhost:8009')
|
const requestClient = new RequestClient('https://localhost:8009')
|
||||||
const randomError = 'some error'
|
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)]
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface LoginResult {
|
|||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
userName: string
|
userName: string
|
||||||
userLongName: string
|
userLongName: string
|
||||||
|
errorMessage?: string
|
||||||
}
|
}
|
||||||
export interface LoginResultInternal {
|
export interface LoginResultInternal {
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
|
|||||||
@@ -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 * as https from 'https'
|
||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import { VerboseMode } from '../types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the configuration for the SASjs instance - eg where and how to
|
* 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.
|
* Set to `true` to enable additional debugging.
|
||||||
*/
|
*/
|
||||||
debug: boolean = true
|
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.
|
* The name of the compute context to use when calling the Viya services directly.
|
||||||
* Example value: 'SAS Job Execution compute context'
|
* 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
|
||||||
|
}
|
||||||
@@ -6,12 +6,10 @@ export * from './Job'
|
|||||||
export * from './JobDefinition'
|
export * from './JobDefinition'
|
||||||
export * from './JobResult'
|
export * from './JobResult'
|
||||||
export * from './Link'
|
export * from './Link'
|
||||||
export * from './Login'
|
|
||||||
export * from './SASjsConfig'
|
export * from './SASjsConfig'
|
||||||
export * from './RequestClient'
|
export * from './SASjsRequest'
|
||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './UploadFile'
|
export * from './UploadFile'
|
||||||
export * from './PollOptions'
|
export * from './PollOptions'
|
||||||
export * from './WriteStream'
|
export * from './WriteStream'
|
||||||
export * from './ExecuteScript'
|
export * from './ExecuteScript'
|
||||||
export * from './errors'
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SASjsRequest } from '../types'
|
import { SASjsRequest } from '../types/SASjsRequest'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparator for SASjs request timestamps.
|
* 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 './asyncForEach'
|
||||||
export * from './compareTimestamps'
|
export * from './compareTimestamps'
|
||||||
export * from './convertToCsv'
|
export * from './convertToCsv'
|
||||||
|
export * from './constants'
|
||||||
export * from './createAxiosInstance'
|
export * from './createAxiosInstance'
|
||||||
export * from './delay'
|
export * from './delay'
|
||||||
export * from './fetchLogByChunks'
|
export * from './fetchLogByChunks'
|
||||||
|
|||||||
Reference in New Issue
Block a user