1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-01 17:50:06 +00:00

Compare commits

...

22 Commits

Author SHA1 Message Date
Yury Shkoda
04d17c3680 Merge pull request #153 from sasjs/context-issue
fix(context): fixed log parsing
2020-11-19 16:53:48 +03:00
Yury Shkoda
d26e15f91c Merge branch 'master' into context-issue 2020-11-19 16:43:30 +03:00
Yury Shkoda
83c46091b3 fix(context): fixed log parsing 2020-11-19 16:39:47 +03:00
Krishna Acondy
d640d7c040 Merge pull request #152 from sasjs/issue117
fix: viya login issue
2020-11-18 17:16:26 +00:00
Mihajlo Medjedovic
c934eb2b08 test: added test for multiple login attempts 2020-11-18 14:13:13 +01:00
Mihajlo Medjedovic
24dd5e32ad style: lint 2020-11-18 13:10:29 +01:00
Mihajlo Medjedovic
a23103b2c3 fix: viya login issue 2020-11-18 13:09:49 +01:00
Yury Shkoda
35aa4235e4 Merge pull request #148 from sasjs/issue113
feat: service not found error handling
2020-11-16 09:54:03 +03:00
Mihajlo Medjedovic
e9be1cf99a fix: service not found error handling 2020-11-12 16:03:32 +01:00
Mihajlo Medjedovic
c7b0821081 style: lint 2020-11-09 18:24:10 +01:00
Mihajlo Medjedovic
4a4618dd32 feat: service not found error handling for SAS9 2020-11-09 18:19:39 +01:00
Krishna Acondy
d223e83c60 Merge pull request #142 from sasjs/issue138
fix(file-uploader): handle errors during file upload
2020-11-02 14:54:58 +00:00
Krishna Acondy
d1f1a20126 chore(file-uploader): move uploader to describe scope 2020-11-02 09:29:33 +00:00
Krishna Acondy
4b89e3762f chore(file-uploader): remove duplication 2020-11-02 08:54:26 +00:00
Krishna Acondy
bc110288de chore(file-uploader): improve mocking of fetch, add tests for all error scenarios 2020-11-02 08:51:27 +00:00
Krishna Acondy
e94e16b52c chore(*): fix linting errors 2020-11-02 07:55:48 +00:00
Krishna Acondy
76aacee016 Merge branch 'master' into issue138 2020-11-02 07:42:39 +00:00
Mihajlo Medjedovic
1a3bd5d1f5 chore: lint 2020-10-30 16:13:03 +01:00
Mihajlo Medjedovic
3f6e89d716 fix: file uploader error handling and tests 2020-10-30 16:11:50 +01:00
Mihajlo Medjedovic
7279c23fe2 fix: FIleUploader added catch 2020-10-27 14:50:05 +01:00
Mihajlo Medjedovic
80707d77d9 gitfe Merge branches 'errorResponse' and 'master' of github.com:sasjs/adapter 2020-10-27 14:40:41 +01:00
Mihajlo Medjedovic
3f796b300d fix: ErrorResponse body changed to error 2020-10-07 11:15:00 +02:00
9 changed files with 282 additions and 76 deletions

View File

@@ -14,4 +14,5 @@ What code changes have been made to achieve the intent.
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).

View File

@@ -37,6 +37,17 @@ export const basicTests = (
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
title: "Multiple Log in attempts",
description: "Should fail on first attempt and should log the user in on second attempt",
test: async () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
title: "Default config",
description:

View File

@@ -1,6 +1,7 @@
import { isLogInRequired, needsRetry, isUrl } from './utils'
import { CsrfToken } from './types/CsrfToken'
import { UploadFile } from './types/UploadFile'
import { ErrorResponse } from './types'
const requestRetryLimit = 5
@@ -18,29 +19,31 @@ export class FileUploader {
private retryCount = 0
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1)
throw new Error('At least one file must be provided.')
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
return new Promise((resolve, reject) => {
if (files?.length < 1)
reject(new ErrorResponse('At least one file must be provided.'))
if (!sasJob || sasJob === '')
reject(new ErrorResponse('sasJob must be provided.'))
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
const formData = new FormData()
for (let file of files) {
@@ -76,7 +79,7 @@ export class FileUploader {
})
.then((responseText) => {
if (isLogInRequired(responseText))
reject('You must be logged in to upload a file')
reject(new ErrorResponse('You must be logged in to upload a file.'))
if (needsRetry(responseText)) {
if (this.retryCount < requestRetryLimit) {
@@ -95,10 +98,18 @@ export class FileUploader {
try {
resolve(JSON.parse(responseText))
} catch (e) {
reject(e)
reject(
new ErrorResponse(
'Error while parsing json from upload response.',
e
)
)
}
}
})
.catch((err: any) => {
reject(new ErrorResponse('Upload request failed.', err))
})
})
}
}

View File

@@ -164,9 +164,9 @@ export class SASViyaApiClient {
for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => {
if (result && result.body && result.body.details) {
if (result && result.error && result.error.details) {
try {
const resultParsed = JSON.parse(result.body.details)
const resultParsed = result.error.details
if (resultParsed && resultParsed.body) {
let sysUserId = ''
@@ -607,7 +607,7 @@ export class SASViyaApiClient {
})
return Promise.reject(
new ErrorResponse('Job execution failed', {
new ErrorResponse('Job execution failed.', {
status: 500,
body: log
})
@@ -1114,7 +1114,7 @@ export class SASViyaApiClient {
}
if (!jobToExecute) {
throw new Error(`The job ${sasJob} was not found.`)
throw new Error(`Job was not found.`)
}
const jobDefinitionLink = jobToExecute?.links.find(
(l) => l.rel === 'getResource'

View File

@@ -411,6 +411,29 @@ export default class SASjs {
}
}
private async getLoginForm(response: any) {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
/**
* Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
@@ -419,10 +442,16 @@ export default class SASjs {
const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
const responseText = await loginResponse.text()
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
let loginForm: any = null
if (!isLoggedIn) {
loginForm = await this.getLoginForm(responseText)
}
return Promise.resolve({
isLoggedIn,
userName: this.userName
userName: this.userName,
loginForm
})
}
@@ -440,7 +469,7 @@ export default class SASjs {
this.userName = loginParams.username
const { isLoggedIn } = await this.checkSession()
const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedIn) {
this.resendWaitingRequests()
@@ -450,15 +479,13 @@ export default class SASjs {
})
}
const loginForm = await this.getLoginForm()
for (const key in loginForm) {
loginParams[key] = loginForm[key]
}
const loginParamsStr = serialize(loginParams)
return fetch(this.loginUrl, {
method: 'post',
method: 'POST',
credentials: 'include',
referrerPolicy: 'same-origin',
body: loginParamsStr,
@@ -780,11 +807,23 @@ export default class SASjs {
} else {
this.retryCountComputeApi = 0
reject(
new ErrorResponse('Compute API retry requests limit reached')
new ErrorResponse('Compute API retry requests limit reached.')
)
}
}
if (response?.log) {
this.appendSasjsRequest(response.log, sasJob, null)
}
if (error.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
if (error && error.status === 401) {
if (loginRequiredCallback) loginRequiredCallback(true)
sasjsWaitingRequest.requestPromise.resolve = resolve
@@ -792,10 +831,8 @@ export default class SASjs {
sasjsWaitingRequest.config = config
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
} else {
reject(new ErrorResponse('Job execution failed', error))
reject(new ErrorResponse('Job execution failed.', error))
}
this.appendSasjsRequest(response.log, sasJob, null)
})
}
)
@@ -875,12 +912,24 @@ export default class SASjs {
} else {
this.retryCountJeseApi = 0
reject(
new ErrorResponse('Jes API retry requests limit reached')
new ErrorResponse('Jes API retry requests limit reached.')
)
}
}
reject(new ErrorResponse('Job execution failed', e))
if (e?.log) {
this.appendSasjsRequest(e.log, sasJob, null)
}
if (e.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
reject(new ErrorResponse('Job execution failed.', e))
})
)
}
@@ -1064,7 +1113,7 @@ export default class SASjs {
} else {
reject(
new ErrorResponse(
'Job WEB execution failed',
'Job WEB execution failed.',
this.parseSAS9ErrorResponse(responseText)
)
)
@@ -1082,7 +1131,7 @@ export default class SASjs {
} catch (e) {
reject(
new ErrorResponse(
'Job WEB debug response parsing failed',
'Job WEB debug response parsing failed.',
{ response: resText, exception: e }
)
)
@@ -1091,7 +1140,7 @@ export default class SASjs {
(err: any) => {
reject(
new ErrorResponse(
'Job WEB debug response parsing failed',
'Job WEB debug response parsing failed.',
err
)
)
@@ -1100,19 +1149,34 @@ export default class SASjs {
} catch (e) {
reject(
new ErrorResponse(
'Job WEB debug response parsing failed',
'Job WEB debug response parsing failed.',
{ response: responseText, exception: e }
)
)
}
} else {
this.updateUsername(responseText)
if (
responseText.includes(
'The requested URL /SASStoredProcess/do/ was not found on this server.'
) ||
responseText.includes('Stored process not found')
) {
reject(
new ErrorResponse(
'Service not found on the server.',
{ service: sasJob },
responseText
)
)
}
try {
const parsedJson = JSON.parse(responseText)
resolve(parsedJson)
} catch (e) {
reject(
new ErrorResponse('Job WEB response parsing failed', {
new ErrorResponse('Job WEB response parsing failed.', {
response: responseText,
exception: e
})
@@ -1123,7 +1187,7 @@ export default class SASjs {
}
})
.catch((e: Error) => {
reject(new ErrorResponse('Job WEB request failed', e))
reject(new ErrorResponse('Job WEB request failed.', e))
})
}
)
@@ -1448,26 +1512,6 @@ export default class SASjs {
}
}
private async getLoginForm() {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const response = await fetch(this.loginUrl).then((r) => r.text())
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
private async createFoldersAndServices(
parentFolder: string,
membersJson: any[],

View File

@@ -0,0 +1,137 @@
import { FileUploader } from '../FileUploader'
import { UploadFile } from '../types'
const sampleResponse = `{
"SYSUSERID": "cas",
"_DEBUG":" ",
"SYS_JES_JOB_URI": "/jobExecution/jobs/000-000-000-000",
"_PROGRAM" : "/Public/app/editors/loadfile",
"SYSCC" : "0",
"SYSJOBID" : "117382",
"SYSWARNINGTEXT" : ""
}`
const prepareFilesAndParams = () => {
const files: UploadFile[] = [
{
file: new File([''], 'testfile'),
fileName: 'testfile'
}
]
const params = { table: 'libtable' }
return { files, params }
}
describe('FileUploader', () => {
let originalFetch: any
const fileUploader = new FileUploader(
'/sample/apploc',
'https://sample.server.com',
'/jobs/path',
null,
null
)
beforeAll(() => {
originalFetch = (global as any).fetch
})
beforeEach(() => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve(sampleResponse)
})
)
})
afterAll(() => {
;(global as any).fetch = originalFetch
})
it('should upload successfully', async (done) => {
const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
expect(JSON.stringify(res)).toEqual(
JSON.stringify(JSON.parse(sampleResponse))
)
done()
})
})
it('should an error when no files are provided', async (done) => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('At least one file must be provided.')
done()
})
})
it('should throw an error when no sasJob is provided', async (done) => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('sasJob must be provided.')
done()
})
})
it('should throw an error when login is required', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('<form action="Logon">')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'You must be logged in to upload a file.'
)
done()
})
})
it('should throw an error when invalid JSON is returned by the server', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('{invalid: "json"')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'Error while parsing json from upload response.'
)
done()
})
})
it('should throw an error when the server request fails', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.reject('{message: "Server error"}')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('Upload request failed.')
done()
})
})
})

View File

@@ -1,4 +1,4 @@
import { parseGeneratedCode } from './index'
import { parseGeneratedCode } from '../../utils/index'
it('should parse generated code', async (done) => {
expect(sampleResponse).toBeTruthy()

View File

@@ -1,4 +1,4 @@
import { parseSourceCode } from './index'
import { parseSourceCode } from '../../utils/index'
it('should parse SAS9 source code', async (done) => {
expect(sampleResponse).toBeTruthy()

View File

@@ -1,17 +1,19 @@
export class ErrorResponse {
body: ErrorBody
error: ErrorBody
constructor(message: string, details?: any) {
let detailsString = ''
let raw
constructor(message: string, details?: any, raw?: any) {
let detailsString = details
try {
detailsString = JSON.stringify(details)
} catch {
raw = details
if (typeof details !== 'object') {
try {
detailsString = JSON.parse(details)
} catch {
raw = details
detailsString = ''
}
}
this.body = {
this.error = {
message,
details: detailsString,
raw