mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3556eb3903 | |||
| 600e561a45 | |||
| 6a161a05ef | |||
| 8db02012e5 | |||
| a01b1a9feb | |||
| e6ec51c7eb |
@@ -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 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`.
|
||||
|
||||
30
src/SASjs.ts
30
src/SASjs.ts
@@ -31,7 +31,6 @@ import {
|
||||
} from './job-execution'
|
||||
import { ErrorResponse } from './types/errors'
|
||||
import { LoginOptions, LoginResult } from './types/Login'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
interface ExecuteScriptParams {
|
||||
linesOfCode: string[]
|
||||
@@ -855,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 to enable verbose mode (log every HTTP response).
|
||||
*/
|
||||
public async startComputeJob(
|
||||
sasJob: string,
|
||||
@@ -865,8 +863,7 @@ export default class SASjs {
|
||||
waitForResult?: boolean,
|
||||
pollOptions?: PollOptions,
|
||||
printPid = false,
|
||||
variables?: MacroVar,
|
||||
verboseMode?: boolean
|
||||
variables?: MacroVar
|
||||
) {
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
@@ -880,9 +877,6 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
if (verboseMode) this.requestClient?.enableVerboseMode()
|
||||
else if (verboseMode === false) this.requestClient?.disableVerboseMode()
|
||||
|
||||
return this.sasViyaApiClient?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
@@ -976,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(
|
||||
@@ -1141,23 +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) => AxiosResponse,
|
||||
errorCallBack?: (response: AxiosResponse) => AxiosResponse
|
||||
) {
|
||||
this.requestClient?.enableVerboseMode(successCallBack, errorCallBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off verbose mode to log every HTTP response.
|
||||
*/
|
||||
public disableVerboseMode() {
|
||||
this.requestClient?.disableVerboseMode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,16 @@ export class AuthManager {
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
await this.performCASSecurityCheck()
|
||||
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||
|
||||
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||
return {
|
||||
isLoggedIn: false,
|
||||
userName: this.userName || '',
|
||||
userLongName: this.userLongName || '',
|
||||
errorMessage: 'Public access has been denied.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { userName, userLongName } = await this.fetchUserName()
|
||||
@@ -149,7 +158,17 @@ export class AuthManager {
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
await this.performCASSecurityCheck()
|
||||
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||
isLoggedIn = false
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName || '',
|
||||
userLongName: this.userLongName || '',
|
||||
errorMessage: 'Public access has been denied.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loginCallback()
|
||||
@@ -166,11 +185,15 @@ export class AuthManager {
|
||||
private async performCASSecurityCheck() {
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient
|
||||
return await this.requestClient
|
||||
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
||||
.catch((err) => {
|
||||
// ignore if resource not found error
|
||||
if (!(err instanceof NotFoundError)) throw err
|
||||
|
||||
return {
|
||||
result: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -387,3 +410,7 @@ const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
|
||||
|
||||
return /You have signed in/gm.test(response)
|
||||
}
|
||||
|
||||
const isPublicAccessDenied = (response: any): boolean => {
|
||||
return /Public access has been denied/gm.test(response)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import axios from 'axios'
|
||||
import {
|
||||
mockedCurrentUserApi,
|
||||
mockLoginAuthoriseRequiredResponse,
|
||||
mockLoginPublicAccessDeniedResponse,
|
||||
mockLoginSuccessResponse
|
||||
} from './mockResponses'
|
||||
import { serialize } from '../../utils'
|
||||
@@ -213,6 +214,61 @@ describe('AuthManager', () => {
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should post a login & a cas_security request to the SAS9 server when not logged in & get rejected due to public access denied', async () => {
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
userLongName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
mockedAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
expect(loginResponse.errorMessage).toEqual(
|
||||
'Public access has been denied.'
|
||||
)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
const casAuthenticationUrl = `${serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
getHeadersJson
|
||||
)
|
||||
})
|
||||
|
||||
it('should return empty username if unable to logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
@@ -422,6 +478,53 @@ describe('AuthManager', () => {
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return error if public account access is denied', async () => {
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: ''
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName
|
||||
})
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.redirectedLogIn({})
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
expect(loginResponse.errorMessage).toEqual(
|
||||
'Public access has been denied.'
|
||||
)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
|
||||
expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return empty username if user unable to re-login via pop up', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SasAuthResponse } from '@sasjs/utils/types'
|
||||
|
||||
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
||||
export const mockLoginSuccessResponse = `You have signed in`
|
||||
export const mockLoginPublicAccessDeniedResponse = `Public access has been denied`
|
||||
|
||||
export const mockAuthResponse: SasAuthResponse = {
|
||||
access_token: 'acc355',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
getValidJson,
|
||||
parseSasViyaDebugResponse,
|
||||
parseWeboutResponse
|
||||
parseWeboutResponse,
|
||||
SASJS_LOGS_SEPARATOR
|
||||
} from '../utils'
|
||||
import { UploadFile } from '../types/UploadFile'
|
||||
import {
|
||||
|
||||
@@ -187,6 +187,12 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
{ result: jsonResponse, log: res.log },
|
||||
extraResponseAttributes
|
||||
)
|
||||
|
||||
if (this.isPublicAccessDenied(jsonResponse))
|
||||
reject(
|
||||
new ErrorResponse('Public access has been denied', responseObject)
|
||||
)
|
||||
|
||||
resolve(responseObject)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
@@ -262,4 +268,8 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
private isPublicAccessDenied = (response: string): boolean => {
|
||||
return /Public access has been denied/gm.test(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
createAxiosInstance
|
||||
} from '../utils'
|
||||
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
||||
import { inspect } from 'util'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
@@ -60,7 +59,6 @@ export interface HttpClient {
|
||||
export class RequestClient implements HttpClient {
|
||||
private requests: SASjsRequest[] = []
|
||||
private requestsLimit: number = 10
|
||||
private httpInterceptor?: number
|
||||
|
||||
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||
protected fileUploadCsrfToken: CsrfToken | undefined
|
||||
@@ -69,14 +67,10 @@ export class RequestClient implements HttpClient {
|
||||
constructor(
|
||||
protected baseUrl: string,
|
||||
httpsAgentOptions?: https.AgentOptions,
|
||||
requestsLimit?: number,
|
||||
verboseMode?: boolean
|
||||
requestsLimit?: number
|
||||
) {
|
||||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||||
|
||||
if (requestsLimit) this.requestsLimit = requestsLimit
|
||||
|
||||
if (verboseMode) this.enableVerboseMode()
|
||||
}
|
||||
|
||||
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
||||
@@ -186,7 +180,6 @@ export class RequestClient implements HttpClient {
|
||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||
withCredentials: true
|
||||
}
|
||||
|
||||
if (contentType === 'text/plain') {
|
||||
requestConfig.transformResponse = undefined
|
||||
}
|
||||
@@ -396,105 +389,6 @@ export class RequestClient implements HttpClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds colors to the string.
|
||||
* @param str - string to be prettified.
|
||||
* @returns - prettified string
|
||||
*/
|
||||
private prettifyString = (str: any) => inspect(str, { colors: true })
|
||||
|
||||
/**
|
||||
* 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 = (response: AxiosResponse) => {
|
||||
const { status, config, request, data: resData } = response
|
||||
const { data: reqData } = config
|
||||
const { _header: reqHeaders, res } = request
|
||||
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 response
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,7 +1,8 @@
|
||||
import { RequestClient } from './RequestClient'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { SASJS_LOGS_SEPARATOR } from '../utils'
|
||||
|
||||
export interface SasjsParsedResponse<T> {
|
||||
interface SasjsParsedResponse<T> {
|
||||
result: T
|
||||
log: string
|
||||
etag: string
|
||||
@@ -44,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
|
||||
}
|
||||
@@ -75,7 +59,7 @@ export class SasjsRequestClient extends RequestClient {
|
||||
|
||||
const returnResult: SasjsParsedResponse<T> = {
|
||||
result: parsedResponse as T,
|
||||
log: log || '',
|
||||
log,
|
||||
etag,
|
||||
status: response.status
|
||||
}
|
||||
@@ -85,6 +69,3 @@ export class SasjsRequestClient extends RequestClient {
|
||||
return returnResult
|
||||
}
|
||||
}
|
||||
|
||||
export const SASJS_LOGS_SEPARATOR =
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import {
|
||||
SASJS_LOGS_SEPARATOR,
|
||||
SasjsRequestClient,
|
||||
SasjsParsedResponse
|
||||
} from '../SasjsRequestClient'
|
||||
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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
} from '../types/errors'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
|
||||
const axiosActual = jest.requireActual('axios')
|
||||
|
||||
@@ -27,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
|
||||
|
||||
@@ -72,187 +80,6 @@ describe('RequestClient', () => {
|
||||
expect(rejectionErrorMessage).toEqual(expectedError.message)
|
||||
})
|
||||
|
||||
describe('defaultInterceptionCallBack', () => {
|
||||
beforeAll(() => {
|
||||
;(process as any).logger = new Logger(LogLevel.Off)
|
||||
})
|
||||
|
||||
it('should log parsed response', () => {
|
||||
jest.spyOn((process as any).logger, 'info')
|
||||
|
||||
const status = 200
|
||||
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 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'
|
||||
}
|
||||
}
|
||||
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 resHeaders = ['content-type', 'application/json']
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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) => {
|
||||
console.log('success')
|
||||
|
||||
return response
|
||||
}
|
||||
const failureCallback = (response: AxiosResponse) => {
|
||||
console.log('failure')
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
requestClient.enableVerboseMode(successCallback, failureCallback)
|
||||
|
||||
expect(interceptorSpy).toHaveBeenCalledWith(
|
||||
successCallback,
|
||||
failureCallback
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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'
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface LoginResult {
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
userLongName: string
|
||||
errorMessage?: string
|
||||
}
|
||||
export interface LoginResultInternal {
|
||||
isLoggedIn: boolean
|
||||
|
||||
@@ -45,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?: boolean = true
|
||||
/**
|
||||
* The name of the compute context to use when calling the Viya services directly.
|
||||
* Example value: 'SAS Job Execution compute context'
|
||||
|
||||
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