mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-08 04:50:06 +00:00
feat(request-client): implemented verbose mode
This commit is contained in:
@@ -863,7 +863,8 @@ export default class SASjs {
|
|||||||
waitForResult?: boolean,
|
waitForResult?: boolean,
|
||||||
pollOptions?: PollOptions,
|
pollOptions?: PollOptions,
|
||||||
printPid = false,
|
printPid = false,
|
||||||
variables?: MacroVar
|
variables?: MacroVar,
|
||||||
|
verboseMode?: boolean
|
||||||
) {
|
) {
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...this.sasjsConfig,
|
||||||
@@ -877,6 +878,9 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verboseMode) this.requestClient?.enableVerboseMode()
|
||||||
|
else this.requestClient?.disableVerboseMode()
|
||||||
|
|
||||||
return this.sasViyaApiClient?.executeComputeJob(
|
return this.sasViyaApiClient?.executeComputeJob(
|
||||||
sasJob,
|
sasJob,
|
||||||
config.contextName,
|
config.contextName,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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 {
|
export interface HttpClient {
|
||||||
get<T>(
|
get<T>(
|
||||||
@@ -59,6 +60,7 @@ export interface HttpClient {
|
|||||||
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
|
||||||
|
|
||||||
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||||
protected fileUploadCsrfToken: CsrfToken | undefined
|
protected fileUploadCsrfToken: CsrfToken | undefined
|
||||||
@@ -70,6 +72,7 @@ export class RequestClient implements HttpClient {
|
|||||||
requestsLimit?: number
|
requestsLimit?: number
|
||||||
) {
|
) {
|
||||||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||||||
|
|
||||||
if (requestsLimit) this.requestsLimit = requestsLimit
|
if (requestsLimit) this.requestsLimit = requestsLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +183,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -389,6 +393,82 @@ export class RequestClient implements HttpClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private prettifyString = (str: any) => inspect(str, { colors: true })
|
||||||
|
|
||||||
|
private parseInterceptedBody = (body: any) => {
|
||||||
|
if (!body) return ''
|
||||||
|
|
||||||
|
let parsedBody
|
||||||
|
|
||||||
|
if (typeof body === 'string') {
|
||||||
|
try {
|
||||||
|
parsedBody = JSON.parse(body)
|
||||||
|
} catch (error) {
|
||||||
|
parsedBody = body
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsedBody = body
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyLines = this.prettifyString(parsedBody).split('\n')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
public enableVerboseMode = (
|
||||||
|
successCallBack = this.defaultInterceptionCallBack,
|
||||||
|
errorCallBack = this.defaultInterceptionCallBack
|
||||||
|
) => {
|
||||||
|
this.httpInterceptor = this.httpClient.interceptors.response.use(
|
||||||
|
successCallBack,
|
||||||
|
errorCallBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
} from '../types/errors'
|
} from '../types/errors'
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
|
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
|
||||||
|
import { AxiosResponse } from 'axios'
|
||||||
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
|
|
||||||
const axiosActual = jest.requireActual('axios')
|
const axiosActual = jest.requireActual('axios')
|
||||||
|
|
||||||
@@ -25,16 +27,6 @@ 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
|
||||||
|
|
||||||
@@ -80,6 +72,187 @@ describe('RequestClient', () => {
|
|||||||
expect(rejectionErrorMessage).toEqual(expectedError.message)
|
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', () => {
|
describe('handleError', () => {
|
||||||
const requestClient = new RequestClient('https://localhost:8009')
|
const requestClient = new RequestClient('https://localhost:8009')
|
||||||
const randomError = 'some error'
|
const randomError = 'some error'
|
||||||
|
|||||||
Reference in New Issue
Block a user