1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-13 02:04:36 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
2c6198ae25 chore: for krishna 2021-09-06 19:28:28 +05:00
45 changed files with 4563 additions and 8234 deletions

View File

@@ -1,7 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: monthly
open-pull-requests-limit: 10
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

2
.gitignore vendored
View File

@@ -5,4 +5,4 @@ build
/coverage
.DS_Store
.DS_Store

View File

@@ -3,4 +3,3 @@ docs/
.github/
*.md
*.spec.ts
.all-contributorsrc

View File

@@ -36,7 +36,7 @@ Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapte
The backend part can be deployed as follows:
```sas
```
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */
@@ -85,11 +85,11 @@ let sasJs = new SASjs.default(
);
```
If you've installed it via NPM, you can import it as a default import like so:
```js
```
import SASjs from '@sasjs/adapter';
```
You can then instantiate it with:
```js
```
const sasJs = new SASjs({your config})
```
@@ -119,7 +119,6 @@ sasJs.request("/path/to/my/service", dataObject)
console.log(response.tablewith2cols1row[0].COL1.value)
})
```
We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:
```javascript
@@ -147,7 +146,6 @@ The adapter will also cache the logs (if debug enabled) and even the work tables
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
The following snippet shows the process of SAS tables arriving / leaving:
```sas
/* fetch all input tables sent from frontend - they arrive as work tables */
%webout(FETCH)
@@ -163,6 +161,7 @@ run;
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
%webout(OBJ,tables,label=newtable) /* rename tables on export */
%webout(CLOSE) /* close the JSON and send some extra useful variables too */
```
## Configuration
@@ -173,7 +172,6 @@ Configuration on the client side involves passing an object on startup, which ca
* `serverType` - either `SAS9` or `SASVIYA`.
* `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.
* `LoginMechanism` - either `Default` or `Redirected`. If `Redirected` then authentication occurs through the injection of an additional screen, which contains the SASLogon prompt. This allows for more complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the redirect flow can also be modified. If left at "Default" then the developer must capture the username and password and use these with the `.login()` method.
* `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`.
@@ -198,7 +196,7 @@ Here we are running Jobs using the Job Execution Service except this time we are
This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
```json
```
{
appLoc:"/Your/Path",
serverType:"SASVIYA",
@@ -212,12 +210,12 @@ This approach is by far the fastest, as a result of the optimisations we have bu
With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
```json
```
{
appLoc:"/Your/Path",
serverType:"SASVIYA",
useComputeApi: true,
contextName: "yourComputeContext"
contextName: 'yourComputeContext'
}
```

2143
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -48,29 +48,29 @@
"copyfiles": "^2.4.1",
"cp": "^0.2.0",
"dotenv": "^10.0.0",
"jest": "^27.2.0",
"jest": "^27.1.0",
"jest-extended": "^0.11.5",
"node-polyfill-webpack-plugin": "^1.1.4",
"path": "^0.12.7",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"semantic-release": "^17.4.7",
"terser-webpack-plugin": "^5.2.4",
"terser-webpack-plugin": "^5.2.0",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "^0.22.3",
"typedoc": "^0.21.9",
"typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "4.3.5",
"webpack": "^5.52.1",
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2"
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "^2.32.0",
"axios": "^0.21.4",
"@sasjs/utils": "^2.30.0",
"axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0",
"https": "^1.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"private": true,
"dependencies": {
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
"@sasjs/test-framework": "^1.4.2",
"@sasjs/test-framework": "^1.4.0",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.41",
"@types/react": "^17.0.1",
@@ -23,7 +23,7 @@
"test": "react-scripts test",
"eject": "react-scripts eject",
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
"deploy:tests": "rsync -avhe ssh ./build/* --delete sabhas@sas.analytium.co.uk:/var/www/html/sabhas/sasjs-test || npm run deploy:tests-win",
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
},

View File

@@ -2,7 +2,7 @@
"userName": "",
"password": "",
"sasJsConfig": {
"serverUrl": "",
"serverUrl": "https://sas.analytium.co.uk/",
"appLoc": "/Public/app",
"serverType": "SASVIYA",
"debug": false,

View File

@@ -14,16 +14,16 @@ const App = (): ReactElement<{}> => {
useEffect(() => {
if (adapter) {
const testSuites = [
basicTests(adapter, config.userName, config.password),
sendArrTests(adapter),
sendObjTests(adapter),
specialCaseTests(adapter),
// basicTests(adapter, config.userName, config.password),
// sendArrTests(adapter),
// sendObjTests(adapter),
// specialCaseTests(adapter),
sasjsRequestTests(adapter)
]
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
testSuites.push(computeTests(adapter))
}
// if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
// testSuites.push(computeTests(adapter))
// }
setTestSuites(testSuites)
}

View File

@@ -1,4 +1,4 @@
import SASjs, { LoginMechanism, SASjsConfig } from '@sasjs/adapter'
import SASjs, { SASjsConfig } from '@sasjs/adapter'
import { TestSuite } from '@sasjs/test-framework'
import { ServerType } from '@sasjs/utils/types'
@@ -13,8 +13,7 @@ const defaultConfig: SASjsConfig = {
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: false,
allowInsecureRequests: false,
loginMechanism: LoginMechanism.Default
allowInsecureRequests: false
}
const customConfig = {
@@ -42,19 +41,6 @@ export const basicTests = (
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
title: 'Fetch username for already logged in user',
description: 'Should log the user in',
test: async () => {
await adapter.logIn(userName, password)
const newAdapterIns = new SASjs(adapter.getSasjsConfig())
return await newAdapterIns.checkSession()
},
assertion: (response: any) =>
response?.isLoggedIn && response?.userName === userName
},
{
title: 'Multiple Log in attempts',
description:
@@ -62,7 +48,7 @@ export const basicTests = (
test: async () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return await adapter.logIn(userName, password)
return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
@@ -78,8 +64,8 @@ export const basicTests = (
'common/sendArr',
stringData,
undefined,
async () => {
await adapter.logIn(userName, password)
() => {
adapter.logIn(userName, password)
}
)
},
@@ -165,7 +151,7 @@ export const basicTests = (
description:
'Should complete successful request with extra attributes present in response',
test: async () => {
const config: Partial<SASjsConfig> = {
const config = {
useComputeApi: false
}

107
src/FileUploader.ts Normal file
View File

@@ -0,0 +1,107 @@
import { isUrl, getValidJson, parseSasViyaDebugResponse } from './utils'
import { UploadFile } from './types/UploadFile'
import { ErrorResponse, LoginRequiredError } from './types/errors'
import { RequestClient } from './request/RequestClient'
import { ServerType } from '@sasjs/utils/types'
import SASjs from './SASjs'
import { Server } from 'https'
import { SASjsConfig } from './types'
import { config } from 'process'
export class FileUploader {
constructor(
private sasjsConfig: SASjsConfig,
private jobsPath: string,
private requestClient: RequestClient
) {
if (this.sasjsConfig.serverUrl) isUrl(this.sasjsConfig.serverUrl)
}
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1)
return Promise.reject(
new ErrorResponse('At least one file must be provided.')
)
if (!sasJob || sasJob === '')
return Promise.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.sasjsConfig.appLoc
? this.sasjsConfig.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const formData = new FormData()
for (let file of files) {
formData.append('file', file.file, file.fileName)
}
const csrfToken = this.requestClient.getCsrfToken('file')
if (csrfToken) formData.append('_csrf', csrfToken.value)
if (this.sasjsConfig.debug) formData.append('_debug', '131')
if (
this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.contextName
)
formData.append('_contextname', this.sasjsConfig.contextName)
const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': 'text/plain'
}
// currently only web approach is supported for file upload
// therefore log is part of response with debug enabled and must be parsed
return this.requestClient
.post(
uploadUrl,
formData,
undefined,
'application/json',
headers,
this.sasjsConfig.debug,
true,
sasJob
)
.then(async (res) => {
if (
this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.debug
) {
const jsonResponse = await parseSasViyaDebugResponse(
res.result as string,
this.requestClient,
this.sasjsConfig.serverUrl
)
return jsonResponse
}
return typeof res.result === 'string'
? getValidJson(res.result)
: res.result
//TODO: append to SASjs requests
})
.catch((err: Error) => {
if (err instanceof LoginRequiredError) {
return Promise.reject(
new ErrorResponse('You must be logged in to upload a file.', err)
)
}
return Promise.reject(
new ErrorResponse('File upload request failed.', err)
)
})
}
}

View File

@@ -51,16 +51,6 @@ export class SASViyaApiClient {
)
private folderMap = new Map<string, Job[]>()
/**
* A helper method used to call appendRequest method of RequestClient
* @param response - response from sasjs request
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest(response: any, program: string, debug: boolean) {
this.requestClient!.appendRequest(response, program, debug)
}
public get debug() {
return this._debug
}
@@ -792,12 +782,24 @@ export class SASViyaApiClient {
jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`,
access_token,
'text/plain'
'text/plain',
{},
debug,
true,
sasJob
)
}
if (debug && logLink) {
log = await this.requestClient
.get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
.get<any>(
`${this.serverUrl}${logLink.href}/content`,
access_token,
'application/json',
{},
debug,
true,
sasJob
)
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
}
if (jobStatus === 'failed') {

View File

@@ -1,17 +1,8 @@
import { compareTimestamps, asyncForEach } from './utils'
import {
SASjsConfig,
UploadFile,
EditContextInput,
PollOptions,
LoginMechanism,
FolderMember,
ServiceMember,
ExecutionQuery
} from './types'
import { SASjsConfig, UploadFile, EditContextInput, PollOptions } from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { SASjsApiClient } from './SASjsApiClient'
import { FileUploader } from './FileUploader'
import { AuthManager } from './auth'
import {
ServerType,
@@ -25,11 +16,9 @@ import {
WebJobExecutor,
ComputeJobExecutor,
JesJobExecutor,
Sas9JobExecutor,
FileUploader
Sas9JobExecutor
} from './job-execution'
import { ErrorResponse } from './types/errors'
import { LoginOptions, LoginResult } from './types/Login'
const defaultConfig: SASjsConfig = {
serverUrl: '',
@@ -40,8 +29,7 @@ const defaultConfig: SASjsConfig = {
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: null,
allowInsecureRequests: false,
loginMechanism: LoginMechanism.Default
allowInsecureRequests: false
}
/**
@@ -53,7 +41,6 @@ export default class SASjs {
private jobsPath: string = ''
private sasViyaApiClient: SASViyaApiClient | null = null
private sas9ApiClient: SAS9ApiClient | null = null
private SASjsApiClient: SASjsApiClient | null = null
private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null
@@ -63,6 +50,7 @@ export default class SASjs {
private sas9JobExecutor: JobExecutor | null = null
constructor(config?: any) {
console.log('from SASjs constructor')
this.sasjsConfig = {
...defaultConfig,
...config
@@ -512,7 +500,7 @@ export default class SASjs {
...this.sasjsConfig,
...config
}
this.setupConfiguration()
await this.setupConfiguration()
}
/**
@@ -539,27 +527,8 @@ export default class SASjs {
* @param username - a string representing the username.
* @param password - a string representing the password.
*/
public async logIn(
username?: string,
password?: string,
options: LoginOptions = {}
): Promise<LoginResult> {
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
if (!username || !password) {
throw new Error(
'A username and password are required when using the default login mechanism.'
)
}
return this.authManager!.logIn(username, password)
}
if (typeof window === typeof undefined) {
throw new Error(
'The redirected login mechanism is only available for use in the browser.'
)
}
return this.authManager!.redirectedLogIn(options)
public async logIn(username: string, password: string) {
return this.authManager!.logIn(username, password)
}
/**
@@ -576,32 +545,24 @@ export default class SASjs {
* Process). Is prepended at runtime with the value of `appLoc`.
* @param files - array of files to be uploaded, including File object and file name.
* @param params - request URL parameters.
* @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config,
* for that particular function call.
* @param loginRequiredCallback - a function that is called if the
* user is not logged in (eg to display a login form). The request will be
* resubmitted after successful login.
* @param overrideSasjsConfig - object to override existing config (optional)
*/
public async uploadFile(
public uploadFile(
sasJob: string,
files: UploadFile[],
params: { [key: string]: any } | null,
config: { [key: string]: any } = {},
loginRequiredCallback?: () => any
params: any,
overrideSasjsConfig?: any
) {
config = {
...this.sasjsConfig,
...config
}
const data = { files, params }
const fileUploader = overrideSasjsConfig
? new FileUploader(
{ ...this.sasjsConfig, ...overrideSasjsConfig },
this.jobsPath,
this.requestClient!
)
: this.fileUploader ||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
return await this.fileUploader!.execute(
sasJob,
data,
config,
loginRequiredCallback
)
return fileUploader.uploadFile(sasJob, files, params)
}
/**
@@ -651,6 +612,7 @@ export default class SASjs {
config.useComputeApi !== null
) {
if (config.useComputeApi) {
console.log(615)
return await this.computeJobExecutor!.execute(
sasJob,
data,
@@ -829,14 +791,6 @@ export default class SASjs {
)
}
public async deployToSASjs(members: [FolderMember, ServiceMember]) {
return await this.SASjsApiClient?.deploy(members, this.sasjsConfig.appLoc)
}
public async executeJobSASjs(query: ExecutionQuery) {
return await this.SASjsApiClient?.executeJob(query)
}
/**
* Kicks off execution of the given job via the compute API.
* @returns an object representing the compute session created for the given job.
@@ -895,7 +849,6 @@ export default class SASjs {
await this.webJobExecutor?.resendWaitingRequests()
await this.computeJobExecutor?.resendWaitingRequests()
await this.jesJobExecutor?.resendWaitingRequests()
await this.fileUploader?.resendWaitingRequests()
}
/**
@@ -932,8 +885,10 @@ export default class SASjs {
* @returns SASjsRequest[]
*/
public getSasRequests() {
const requests = [...this.requestClient!.getRequests()]
console.log('from getSASRequests')
const requests = this.requestClient!.getRequests()
const sortedRequests = requests.sort(compareTimestamps)
console.log('sortedRequests', sortedRequests)
return sortedRequests
}
@@ -961,17 +916,10 @@ export default class SASjs {
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
}
if (!this.requestClient) {
this.requestClient = new RequestClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.allowInsecureRequests
)
} else {
this.requestClient.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.allowInsecureRequests
)
}
this.requestClient = new RequestClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.allowInsecureRequests
)
this.jobsPath =
this.sasjsConfig.serverType === ServerType.SasViya
@@ -986,49 +934,34 @@ export default class SASjs {
)
if (this.sasjsConfig.serverType === ServerType.SasViya) {
if (this.sasViyaApiClient) {
if (this.sasViyaApiClient)
this.sasViyaApiClient!.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc
)
} else {
else
this.sasViyaApiClient = new SASViyaApiClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc,
this.sasjsConfig.contextName,
this.requestClient
)
}
this.sasViyaApiClient.debug = this.sasjsConfig.debug
}
if (this.sasjsConfig.serverType === ServerType.Sas9) {
if (this.sas9ApiClient) {
if (this.sas9ApiClient)
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
} else {
else
this.sas9ApiClient = new SAS9ApiClient(
this.sasjsConfig.serverUrl,
this.jobsPath,
this.sasjsConfig.allowInsecureRequests
)
}
}
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (this.SASjsApiClient) {
this.SASjsApiClient.setConfig(this.sasjsConfig.serverUrl)
} else {
this.SASjsApiClient = new SASjsApiClient(
this.sasjsConfig.serverUrl,
this.requestClient
)
}
}
this.fileUploader = new FileUploader(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.sasjsConfig,
this.jobsPath,
this.requestClient
)

View File

@@ -1,39 +0,0 @@
import { FolderMember, ServiceMember, ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient'
export class SASjsApiClient {
constructor(
private serverUrl: string,
private requestClient: RequestClient
) {}
public setConfig(serverUrl: string) {
if (serverUrl) this.serverUrl = serverUrl
}
public async deploy(members: [FolderMember, ServiceMember], appLoc: string) {
const { result } = await this.requestClient.post<{
status: string
message: string
example?: {}
}>(
'SASjsApi/drive/deploy',
{ fileTree: members, appLoc: appLoc },
undefined
)
return Promise.resolve(result)
}
public async executeJob(query: ExecutionQuery) {
const { result } = await this.requestClient.post<{
status: string
message: string
log?: string
logPath?: string
error?: {}
}>('SASjsApi/stp/execute', query, undefined)
return Promise.resolve(result)
}
}

View File

@@ -239,7 +239,15 @@ export async function executeScript(
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
jobResult = await requestClient
.get<any>(resultLink, access_token, 'text/plain')
.get<any>(
resultLink,
access_token,
'text/plain',
{},
debug,
true,
jobPath
)
.catch(async (e) => {
if (e instanceof NotFoundError) {
if (logLink) {

View File

@@ -4,7 +4,7 @@ import { getTokens } from '../../auth/getTokens'
import { RequestClient } from '../../request/RequestClient'
import { JobStatePollError } from '../../types/errors'
import { Link, WriteStream } from '../../types'
import { delay, isNode } from '../../utils'
import { isNode } from '../../utils'
export async function pollJobState(
requestClient: RequestClient,
@@ -246,3 +246,5 @@ const doPoll = async (
return { state, pollCount }
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -1,16 +1,11 @@
import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient'
import { LoginOptions, LoginResult } from '../types/Login'
import { serialize } from '../utils'
import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin'
export class AuthManager {
public userName = ''
private loginUrl: string
private logoutUrl: string
private redirectedLoginUrl = `/SASLogon/home`
constructor(
private serverUrl: string,
private serverType: ServerType,
@@ -24,137 +19,65 @@ export class AuthManager {
: '/SASLogon/logout.do?'
}
/**
* Opens Pop up window to SAS Login screen.
* And checks if user has finished login process.
*/
public async redirectedLogIn({
onLoggedOut
}: LoginOptions): Promise<LoginResult> {
const { isLoggedIn: isLoggedInAlready, userName: currentSessionUsername } =
await this.fetchUserName()
if (isLoggedInAlready) {
await this.loginCallback()
return {
isLoggedIn: true,
userName: currentSessionUsername
}
}
const loginPopup = await openWebPage(
this.redirectedLoginUrl,
'SASLogon',
{
width: 500,
height: 600
},
onLoggedOut
)
if (!loginPopup) {
return { isLoggedIn: false, userName: '' }
}
const { isLoggedIn } =
this.serverType === ServerType.SasViya
? await verifySasViyaLogin(loginPopup)
: await verifySas9Login(loginPopup)
loginPopup.close()
if (isLoggedIn) {
if (this.serverType === ServerType.Sas9) {
await this.performCASSecurityCheck()
}
const { userName } = await this.fetchUserName()
await this.loginCallback()
return { isLoggedIn: true, userName }
}
return { isLoggedIn: false, userName: '' }
}
/**
* Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @returns - a boolean `isLoggedin` and a string `username`
*/
public async logIn(username: string, password: string): Promise<LoginResult> {
const loginParams = {
public async logIn(username: string, password: string) {
const loginParams: any = {
_service: 'default',
username,
password
}
let {
isLoggedIn: isLoggedInAlready,
loginForm,
userName: currentSessionUsername
} = await this.checkSession()
this.userName = loginParams.username
if (isLoggedInAlready) {
if (currentSessionUsername === loginParams.username) {
await this.loginCallback()
const { isLoggedIn, loginForm } = await this.checkSession()
this.userName = currentSessionUsername!
return {
isLoggedIn: true,
userName: this.userName
}
} else {
await this.logOut()
loginForm = await this.getNewLoginForm()
if (isLoggedIn) {
await this.loginCallback()
return {
isLoggedIn,
userName: this.userName
}
} else this.userName = ''
}
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let isLoggedIn = isLogInSuccess(loginResponse)
let loggedIn = isLogInSuccess(loginResponse)
if (!isLoggedIn) {
if (!loggedIn) {
if (isCredentialsVerifyError(loginResponse)) {
const newLoginForm = await this.getLoginForm(loginResponse)
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
}
const res = await this.checkSession()
isLoggedIn = res.isLoggedIn
if (isLoggedIn) this.userName = res.userName
} else {
this.userName = loginParams.username
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
}
if (isLoggedIn) {
if (loggedIn) {
if (this.serverType === ServerType.Sas9) {
await this.performCASSecurityCheck()
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
await this.requestClient.get<string>(
`/SASLogon/login?service=${casAuthenticationUrl}`,
undefined
)
}
this.loginCallback()
} else this.userName = ''
}
return {
isLoggedIn,
isLoggedIn: !!loggedIn,
userName: this.userName
}
}
private async performCASSecurityCheck() {
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
await this.requestClient.get<string>(
`/SASLogon/login?service=${casAuthenticationUrl}`,
undefined
)
}
private async sendLoginRequest(
loginForm: { [key: string]: any },
loginParams: { [key: string]: any }
@@ -180,53 +103,14 @@ export class AuthManager {
/**
* Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing three values
* - a boolean `isLoggedIn`
* - a string `userName` and
* - a form `loginForm` if not loggedin.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
*/
public async checkSession(): Promise<{
isLoggedIn: boolean
userName: string
loginForm?: any
}> {
const { isLoggedIn, userName } = await this.fetchUserName()
let loginForm = null
if (!isLoggedIn) {
//We will logout to make sure cookies are removed and login form is presented
//Residue can happen in case of session expiration
await this.logOut()
loginForm = await this.getNewLoginForm()
}
return Promise.resolve({
isLoggedIn,
userName: userName.toLowerCase(),
loginForm
})
}
private async getNewLoginForm() {
const { result: formResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''),
undefined,
'text/plain'
)
return await this.getLoginForm(formResponse)
}
private async fetchUserName(): Promise<{
isLoggedIn: boolean
userName: string
}> {
public async checkSession() {
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
//For SAS9 we will send request on SASStoredProcess
const url =
this.serverType === ServerType.SasViya
? `${this.serverUrl}/identities/users/@currentUser`
this.serverType === 'SASVIYA'
? `${this.serverUrl}/identities`
: `${this.serverUrl}/SASStoredProcess`
const { result: loginResponse } = await this.requestClient
@@ -236,29 +120,27 @@ export class AuthManager {
})
const isLoggedIn = loginResponse !== 'authErr'
const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
let loginForm = null
return { isLoggedIn, userName }
}
if (!isLoggedIn) {
//We will logout to make sure cookies are removed and login form is presented
//Residue can happen in case of session expiration
await this.logOut()
private extractUserName = (response: any): string => {
switch (this.serverType) {
case ServerType.SasViya:
return response?.id
const { result: formResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''),
undefined,
'text/plain'
)
case ServerType.Sas9:
const matched = response?.match(/"title":"Log Off [0-1a-zA-Z ]*"/)
const username = matched?.[0].slice(17, -1)
if (!username.includes(' ')) return username
return username
.split(' ')
.map((name: string) => name.slice(0, 3).toLowerCase())
.join('')
default:
return ''
loginForm = await this.getLoginForm(formResponse)
}
return Promise.resolve({
isLoggedIn,
userName: this.userName,
loginForm
})
}
private getLoginForm(response: any) {

View File

@@ -1,40 +0,0 @@
import { openLoginPrompt } from '../utils/loginPrompt'
interface WindowFeatures {
width: number
height: number
}
const defaultWindowFeatures: WindowFeatures = { width: 500, height: 600 }
export async function openWebPage(
url: string,
windowName: string = '',
WindowFeatures: WindowFeatures = defaultWindowFeatures,
onLoggedOut?: () => Promise<Boolean>
): Promise<Window | null> {
const { width, height } = WindowFeatures
const left = screen.width / 2 - width / 2
const top = screen.height / 2 - height / 2
const loginPopup = window.open(
url,
windowName,
`toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
)
if (!loginPopup) {
const getUserAction: () => Promise<Boolean> = onLoggedOut ?? openLoginPrompt
const doLogin = await getUserAction()
return doLogin
? window.open(
url,
windowName,
`toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
)
: null
}
return loginPopup
}

View File

@@ -3,14 +3,10 @@ import * as dotenv from 'dotenv'
import { ServerType } from '@sasjs/utils/types'
import axios from 'axios'
import {
mockedCurrentUserApi,
mockLoginAuthoriseRequiredResponse,
mockLoginSuccessResponse
} from './mockResponses'
import { serialize } from '../../utils'
import * as openWebPageModule from '../openWebPage'
import * as verifySasViyaLoginModule from '../verifySasViyaLogin'
import * as verifySas9LoginModule from '../verifySas9Login'
import { RequestClient } from '../../request/RequestClient'
jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios>
@@ -61,614 +57,134 @@ describe('AuthManager', () => {
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
})
describe('login - default mechanism', () => {
it('should call the auth callback and return when already logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
userName,
loginForm: 'test'
})
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should post a login request to the server when already logged in with other username', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
userName: 'someOtherUsername',
loginForm: null
})
)
jest
.spyOn(authManager, 'logOut')
.mockImplementation(() => Promise.resolve(true))
jest
.spyOn<any, any>(authManager, 'getNewLoginForm')
.mockImplementation(() =>
Promise.resolve({
name: 'test'
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
const loginParams = serialize({
_service: 'default',
username: userName,
password,
name: 'test'
it('should call the auth callback and return when already logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
userName: 'test',
loginForm: 'test'
})
expect(authCallback).toHaveBeenCalledTimes(1)
expect(authManager.logOut).toHaveBeenCalledTimes(1)
expect(authManager['getNewLoginForm']).toHaveBeenCalledTimes(1)
expect(mockedAxios.post).toHaveBeenCalledWith(
`/SASLogon/login`,
loginParams,
{
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
}
}
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
)
it('should post a login request to the server when not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: '',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
const loginResponse = await authManager.logIn(userName, password)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
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: '*/*'
}
}
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should post a login & a cas_security request to the SAS9 server when not logged in', async () => {
const serverType = ServerType.Sas9
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: '',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 }))
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
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
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should return empty username if unable to logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: '',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: 'Not Signed in' })
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeFalsy()
expect(loginResponse.userName).toEqual('')
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: '*/*'
}
}
)
})
it('should parse and submit the authorisation form when necessary', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn(requestClient, 'authorize')
.mockImplementation(() => Promise.resolve())
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: 'test',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementationOnce(() =>
Promise.resolve({
data: mockLoginAuthoriseRequiredResponse,
config: { url: 'https://test.com/SASLogon/login' },
request: { responseURL: 'https://test.com/OAuth/authorize' }
})
)
mockedAxios.get.mockImplementationOnce(() =>
Promise.resolve({
data: mockLoginAuthoriseRequiredResponse
})
)
await authManager.logIn(userName, password)
expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse
)
})
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(authCallback).toHaveBeenCalledTimes(1)
})
describe('login - redirect mechanism', () => {
beforeAll(() => {
jest.mock('../openWebPage')
jest
.spyOn(openWebPageModule, 'openWebPage')
.mockImplementation(() =>
Promise.resolve({ close: jest.fn() } as unknown as Window)
)
jest.mock('../verifySasViyaLogin')
jest
.spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin')
.mockImplementation(() => Promise.resolve({ isLoggedIn: true }))
jest.mock('../verifySas9Login')
jest
.spyOn(verifySas9LoginModule, 'verifySas9Login')
.mockImplementation(() => Promise.resolve({ isLoggedIn: true }))
})
it('should call the auth callback and return when already logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn<any, any>(authManager, 'fetchUserName')
.mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
userName
})
)
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should perform login via pop up if not logged in', async () => {
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
})
)
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2)
expect(verifySasViyaLoginModule.verifySasViyaLogin).toHaveBeenCalledTimes(
1
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should perform login via pop up if not logged in with server sas9', 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
})
)
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2)
expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should return empty username if user unable to re-login via pop up', async () => {
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
})
)
jest
.spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin')
.mockImplementation(() => Promise.resolve({ isLoggedIn: false }))
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeFalsy()
expect(loginResponse.userName).toEqual('')
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
expect(authCallback).toHaveBeenCalledTimes(0)
})
it('should return empty username if user rejects to re-login', async () => {
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
})
)
jest
.spyOn(openWebPageModule, 'openWebPage')
.mockImplementation(() => Promise.resolve(null))
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeFalsy()
expect(loginResponse.userName).toEqual('')
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
expect(authCallback).toHaveBeenCalledTimes(0)
it('should post a login request to the server if not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: 'test',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
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: '*/*'
}
}
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
describe('checkSession', () => {
it('return session information when logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({ data: mockedCurrentUserApi(userName) })
)
it('should parse and submit the authorisation form when necessary', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn(requestClient, 'authorize')
.mockImplementation(() => Promise.resolve())
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: 'test',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementationOnce(() =>
Promise.resolve({
data: mockLoginAuthoriseRequiredResponse,
config: { url: 'https://test.com/SASLogon/login' },
request: { responseURL: 'https://test.com/OAuth/authorize' }
})
)
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(response.userName).toEqual(userName)
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/identities/users/@currentUser`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
mockedAxios.get.mockImplementationOnce(() =>
Promise.resolve({
data: mockLoginAuthoriseRequiredResponse
})
)
await authManager.logIn(userName, password)
expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse
)
})
it('should check and return session information if logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({ data: '<button onClick="logout">' })
)
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/identities`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
)
})
it('return session information when logged in - SAS9', async () => {
// username cannot have `-` and cannot be uppercased
const username = 'testusername'
const serverType = ServerType.Sas9
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({
data: `"title":"Log Off ${username}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })`
})
)
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(response.userName).toEqual(username)
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/SASStoredProcess`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
}
)
})
it('return session information when logged in - SAS9 - having full name in html', async () => {
const fullname = 'FirstName LastName'
const username = 'firlas'
const serverType = ServerType.Sas9
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({
data: `"title":"Log Off ${fullname}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })`
})
)
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(response.userName).toEqual(username)
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/SASStoredProcess`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
}
)
})
it('perform logout when not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get
.mockImplementationOnce(() => Promise.resolve({ status: 401 }))
.mockImplementation(() => Promise.resolve({}))
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeFalsy()
expect(response.userName).toEqual('')
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/identities/users/@currentUser`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
}
)
expect(mockedAxios.get).toHaveBeenNthCalledWith(
2,
`/SASLogon/logout.do?`,
getHeadersJson
)
})
}
)
})
})
const getHeadersJson = {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
responseType: 'json'
}

View File

@@ -22,28 +22,3 @@ export const generateToken = (timeToLiveSeconds: number): string => {
const token = `${header}.${payload}.${signature}`
return token
}
export const mockedCurrentUserApi = (username: string) => ({
creationTimeStamp: '2021-04-17T14:13:14.000Z',
modifiedTimeStamp: '2021-08-31T22:08:07.000Z',
id: username,
type: 'user',
name: 'Full User Name',
links: [
{
method: 'GET',
rel: 'self',
href: `/identities/users/${username}`,
uri: `/identities/users/${username}`,
type: 'user'
},
{
method: 'GET',
rel: 'alternate',
href: `/identities/users/${username}`,
uri: `/identities/users/${username}`,
type: 'application/vnd.sas.summary'
}
],
version: 2
})

View File

@@ -1,64 +0,0 @@
/**
* @jest-environment jsdom
*/
import { openWebPage } from '../openWebPage'
import * as loginPromptModule from '../../utils/loginPrompt'
describe('openWebPage', () => {
const serverUrl = 'http://test-server.com'
describe('window.open is not blocked', () => {
const mockedOpen = jest
.fn()
.mockImplementation(() => ({} as unknown as Window))
const originalOpen = window.open
beforeAll(() => {
window.open = mockedOpen
})
afterAll(() => {
window.open = originalOpen
})
it(`should return new Window popup - using default adapter's dialog`, async () => {
await expect(openWebPage(serverUrl)).resolves.toBeDefined()
expect(mockedOpen).toBeCalled()
})
})
describe('window.open is blocked', () => {
const mockedOpen = jest.fn().mockImplementation(() => null)
const originalOpen = window.open
beforeAll(() => {
window.open = mockedOpen
})
afterAll(() => {
window.open = originalOpen
})
it(`should return new Window popup - using default adapter's dialog`, async () => {
jest.mock('../../utils/loginPrompt')
jest
.spyOn(loginPromptModule, 'openLoginPrompt')
.mockImplementation(() => Promise.resolve(true))
await expect(openWebPage(serverUrl)).resolves.toBeDefined()
expect(loginPromptModule.openLoginPrompt).toBeCalled()
expect(mockedOpen).toBeCalled()
})
it(`should return new Window popup - using frontend's provided onloggedOut`, async () => {
const onLoggedOut = jest
.fn()
.mockImplementation(() => Promise.resolve(true))
await expect(
openWebPage(serverUrl, undefined, undefined, onLoggedOut)
).resolves.toBeDefined()
expect(onLoggedOut).toBeCalled()
expect(mockedOpen).toBeCalled()
})
})
})

View File

@@ -1,37 +0,0 @@
/**
* @jest-environment jsdom
*/
import { verifySas9Login } from '../verifySas9Login'
import * as delayModule from '../../utils/delay'
describe('verifySas9Login', () => {
const serverUrl = 'http://test-server.com'
beforeAll(() => {
jest.mock('../../utils')
jest
.spyOn(delayModule, 'delay')
.mockImplementation(() => Promise.resolve({}))
})
it('should return isLoggedIn true by checking state of popup', async () => {
const popup = {
window: {
location: { href: serverUrl + `/SASLogon/home` },
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
}
} as unknown as Window
await expect(verifySas9Login(popup)).resolves.toEqual({
isLoggedIn: true
})
})
it('should return isLoggedIn false if user closed popup, already', async () => {
const popup: Window = { closed: true } as unknown as Window
await expect(verifySas9Login(popup)).resolves.toEqual({
isLoggedIn: false
})
})
})

View File

@@ -1,38 +0,0 @@
/**
* @jest-environment jsdom
*/
import { verifySasViyaLogin } from '../verifySasViyaLogin'
import * as delayModule from '../../utils/delay'
describe('verifySasViyaLogin', () => {
const serverUrl = 'http://test-server.com'
beforeAll(() => {
jest.mock('../../utils')
jest
.spyOn(delayModule, 'delay')
.mockImplementation(() => Promise.resolve({}))
document.cookie = encodeURIComponent('Current-User={"userId":"user-hash"}')
})
it('should return isLoggedIn true by checking state of popup', async () => {
const popup = {
window: {
location: { href: serverUrl + `/SASLogon/home` },
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
}
} as unknown as Window
await expect(verifySasViyaLogin(popup)).resolves.toEqual({
isLoggedIn: true
})
})
it('should return isLoggedIn false if user closed popup, already', async () => {
const popup: Window = { closed: true } as unknown as Window
await expect(verifySasViyaLogin(popup)).resolves.toEqual({
isLoggedIn: false
})
})
})

View File

@@ -1,20 +0,0 @@
import { delay } from '../utils'
export async function verifySas9Login(loginPopup: Window): Promise<{
isLoggedIn: boolean
}> {
let isLoggedIn = false
let startTime = new Date()
let elapsedSeconds = 0
do {
await delay(1000)
if (loginPopup.closed) break
isLoggedIn =
loginPopup.window.location.href.includes('SASLogon') &&
loginPopup.window.document.body.innerText.includes('You have signed in.')
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
return { isLoggedIn }
}

View File

@@ -1,33 +0,0 @@
import { delay } from '../utils'
export async function verifySasViyaLogin(loginPopup: Window): Promise<{
isLoggedIn: boolean
}> {
let isLoggedIn = false
let startTime = new Date()
let elapsedSeconds = 0
do {
await delay(1000)
if (loginPopup.closed) break
isLoggedIn = isLoggedInSASVIYA()
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
let isAuthorized = false
startTime = new Date()
do {
await delay(1000)
if (loginPopup.closed) break
isAuthorized =
loginPopup.window.location.href.includes('SASLogon') ||
loginPopup.window.document.body?.innerText?.includes(
'You have signed in.'
)
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isAuthorized && elapsedSeconds < 5 * 60)
return { isLoggedIn: isLoggedIn && isAuthorized }
}
export const isLoggedInSASVIYA = () =>
document.cookie.includes('Current-User') && document.cookie.includes('userId')

View File

@@ -35,16 +35,16 @@ export class ComputeJobExecutor extends BaseJobExecutor {
expectWebout
)
.then((response) => {
this.sasViyaApiClient.appendRequest(response, sasJob, config.debug)
console.log('then block of compute job executor')
resolve(response.result)
})
.catch(async (e: Error) => {
if (e instanceof ComputeJobExecutionError) {
this.sasViyaApiClient.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e))
}
if (e instanceof LoginRequiredError) {
await loginCallback()
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
@@ -60,8 +60,6 @@ export class ComputeJobExecutor extends BaseJobExecutor {
}
)
})
await loginCallback()
} else {
reject(new ErrorResponse(e?.message, e))
}

View File

@@ -1,143 +0,0 @@
import {
getValidJson,
parseSasViyaDebugResponse,
parseWeboutResponse
} from '../utils'
import { UploadFile } from '../types/UploadFile'
import {
ErrorResponse,
JobExecutionError,
LoginRequiredError
} from '../types/errors'
import { RequestClient } from '../request/RequestClient'
import { ServerType } from '@sasjs/utils/types'
import { BaseJobExecutor } from './JobExecutor'
interface dataFileUpload {
files: UploadFile[]
params: { [key: string]: any } | null
}
export class FileUploader extends BaseJobExecutor {
constructor(
serverUrl: string,
serverType: ServerType,
private jobsPath: string,
private requestClient: RequestClient
) {
super(serverUrl, serverType)
}
public async execute(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any
) {
const { files, params }: dataFileUpload = data
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
if (!files?.length)
throw new ErrorResponse('At least one file must be provided.')
if (!sasJob || sasJob === '')
throw new ErrorResponse('sasJob must be provided.')
let paramsString = ''
for (let param in params)
if (params.hasOwnProperty(param))
paramsString += `&${param}=${params[param]}`
const program = config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const formData = new FormData()
for (let file of files) {
formData.append('file', file.file, file.fileName)
}
const csrfToken = this.requestClient.getCsrfToken('file')
if (csrfToken) formData.append('_csrf', csrfToken.value)
if (config.debug) formData.append('_debug', '131')
if (config.serverType === ServerType.SasViya && config.contextName)
formData.append('_contextname', config.contextName)
const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': 'text/plain'
}
// currently only web approach is supported for file upload
// therefore log is part of response with debug enabled and must be parsed
const requestPromise = new Promise((resolve, reject) => {
this.requestClient
.post(uploadUrl, formData, undefined, 'application/json', headers)
.then(async (res: any) => {
this.requestClient.appendRequest(res, sasJob, config.debug)
let jsonResponse = res.result
if (config.debug) {
switch (this.serverType) {
case ServerType.SasViya:
jsonResponse = await parseSasViyaDebugResponse(
res.result,
this.requestClient,
config.serverUrl
)
break
case ServerType.Sas9:
jsonResponse =
typeof res.result === 'string'
? parseWeboutResponse(res.result, uploadUrl)
: res.result
break
}
} else {
jsonResponse =
typeof res.result === 'string'
? getValidJson(res.result)
: res.result
}
resolve(jsonResponse)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
this.requestClient!.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e))
}
if (e instanceof LoginRequiredError) {
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
data,
config,
loginRequiredCallback
).then(
(res: any) => {
resolve(res)
},
(err: any) => {
reject(err)
}
)
})
await loginCallback()
} else {
reject(new ErrorResponse('File upload request failed.', e))
}
})
})
return requestPromise
}
}

View File

@@ -7,7 +7,6 @@ import {
} from '../types/errors'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { BaseJobExecutor } from './JobExecutor'
import { appendExtraResponseAttributes } from '../utils'
export class JesJobExecutor extends BaseJobExecutor {
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
@@ -28,23 +27,32 @@ export class JesJobExecutor extends BaseJobExecutor {
this.sasViyaApiClient
?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
.then((response: any) => {
this.sasViyaApiClient.appendRequest(response, sasJob, config.debug)
let responseObject = {}
const responseObject = appendExtraResponseAttributes(
response,
extraResponseAttributes
)
if (extraResponseAttributes && extraResponseAttributes.length > 0) {
const extraAttributes = extraResponseAttributes.reduce(
(map: any, obj: any) => ((map[obj] = response[obj]), map),
{}
)
responseObject = {
result: response.result,
...extraAttributes
}
} else {
responseObject = response.result
}
resolve(responseObject)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
this.sasViyaApiClient.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e))
}
if (e instanceof LoginRequiredError) {
await loginCallback()
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
@@ -62,8 +70,6 @@ export class JesJobExecutor extends BaseJobExecutor {
}
)
})
await loginCallback()
} else {
reject(new ErrorResponse(e?.message, e))
}

View File

@@ -1,6 +1,7 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { SASjsRequest } from '../types'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { asyncForEach } from '../utils'
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
export type ExecuteFunction = () => Promise<any>
@@ -20,6 +21,7 @@ export abstract class BaseJobExecutor implements JobExecutor {
constructor(protected serverUrl: string, protected serverType: ServerType) {}
private waitingRequests: ExecuteFunction[] = []
private requests: SASjsRequest[] = []
abstract execute(
sasJob: string,

View File

@@ -45,7 +45,7 @@ export class Sas9JobExecutor extends BaseJobExecutor {
if (data) {
try {
formData = generateFileUploadForm(formData, data)
} catch (e: any) {
} catch (e) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
}

View File

@@ -1,12 +1,9 @@
import {
AuthConfig,
ExtraResponseAttributes,
ServerType
} from '@sasjs/utils/types'
import { ServerType } from '@sasjs/utils/types'
import {
ErrorResponse,
JobExecutionError,
LoginRequiredError
LoginRequiredError,
WeboutResponseError
} from '../types/errors'
import { generateFileUploadForm } from '../file/generateFileUploadForm'
import { generateTableUploadForm } from '../file/generateTableUploadForm'
@@ -14,8 +11,8 @@ import { RequestClient } from '../request/RequestClient'
import { SASViyaApiClient } from '../SASViyaApiClient'
import {
isRelativePath,
parseSasViyaDebugResponse,
appendExtraResponseAttributes
getValidJson,
parseSasViyaDebugResponse
} from '../utils'
import { BaseJobExecutor } from './JobExecutor'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
@@ -40,9 +37,7 @@ export class WebJobExecutor extends BaseJobExecutor {
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = []
loginRequiredCallback?: any
) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
const program = isRelativePath(sasJob)
@@ -53,36 +48,10 @@ export class WebJobExecutor extends BaseJobExecutor {
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
if (config.serverType === ServerType.SasViya) {
let jobUri
try {
jobUri = await this.getJobUri(sasJob)
} catch (e: any) {
return new Promise(async (resolve, reject) => {
if (e instanceof LoginRequiredError) {
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
).then(
(res: any) => {
resolve(res)
},
(err: any) => {
reject(err)
}
)
})
await loginCallback()
} else {
reject(new ErrorResponse(e?.message, e))
}
})
}
const jobUri =
config.serverType === ServerType.SasViya
? await this.getJobUri(sasJob)
: ''
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
@@ -119,7 +88,7 @@ export class WebJobExecutor extends BaseJobExecutor {
// file upload approach
try {
formData = generateFileUploadForm(formData, data)
} catch (e: any) {
} catch (e) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
} else {
@@ -129,7 +98,7 @@ export class WebJobExecutor extends BaseJobExecutor {
generateTableUploadForm(formData, data)
formData = newFormData
requestParams = { ...requestParams, ...params }
} catch (e: any) {
} catch (e) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
}
@@ -142,51 +111,50 @@ export class WebJobExecutor extends BaseJobExecutor {
}
const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, undefined)
this.requestClient!.post(
apiUrl,
formData,
undefined,
'application/json',
{},
config.debug,
true,
sasJob
)
.then(async (res: any) => {
this.requestClient!.appendRequest(res, sasJob, config.debug)
let jsonResponse = res.result
if (config.debug) {
switch (this.serverType) {
case ServerType.SasViya:
jsonResponse = await parseSasViyaDebugResponse(
res.result,
this.requestClient,
this.serverUrl
)
break
case ServerType.Sas9:
jsonResponse =
typeof res.result === 'string'
? parseWeboutResponse(res.result, apiUrl)
: res.result
break
}
if (this.serverType === ServerType.SasViya && config.debug) {
const jsonResponse = await parseSasViyaDebugResponse(
res.result,
this.requestClient,
this.serverUrl
)
resolve(jsonResponse)
}
if (this.serverType === ServerType.Sas9 && config.debug) {
let jsonResponse = res.result
if (typeof res.result === 'string')
jsonResponse = parseWeboutResponse(res.result, apiUrl)
const responseObject = appendExtraResponseAttributes(
{ result: jsonResponse },
extraResponseAttributes
)
resolve(responseObject)
getValidJson(jsonResponse)
resolve(res.result)
}
getValidJson(res.result as string)
resolve(res.result)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
this.requestClient!.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e))
}
if (e instanceof LoginRequiredError) {
await loginCallback()
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
loginRequiredCallback
).then(
(res: any) => {
resolve(res)
@@ -196,8 +164,6 @@ export class WebJobExecutor extends BaseJobExecutor {
}
)
})
await loginCallback()
} else {
reject(new ErrorResponse(e?.message, e))
}

View File

@@ -3,4 +3,3 @@ export * from './JesJobExecutor'
export * from './JobExecutor'
export * from './Sas9JobExecutor'
export * from './WebJobExecutor'
export * from './FileUploader'

View File

@@ -52,14 +52,25 @@ export class RequestClient implements HttpClient {
protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined
protected httpClient!: AxiosInstance
protected httpClient: AxiosInstance
constructor(protected baseUrl: string, allowInsecure = false) {
this.createHttpClient(baseUrl, allowInsecure)
}
const https = require('https')
if (allowInsecure && https.Agent) {
this.httpClient = axios.create({
baseURL: baseUrl,
httpsAgent: new https.Agent({
rejectUnauthorized: !allowInsecure
})
})
} else {
this.httpClient = axios.create({
baseURL: baseUrl
})
}
public setConfig(baseUrl: string, allowInsecure = false) {
this.createHttpClient(baseUrl, allowInsecure)
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 305
}
public getCsrfToken(type: 'general' | 'file' = 'general') {
@@ -94,7 +105,8 @@ export class RequestClient implements HttpClient {
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest(response: any, program: string, debug: boolean) {
public appendRequest = (response: any, program: string, debug: boolean) => {
console.log('from appendRequest')
let sourceCode = ''
let generatedCode = ''
let sasWork = null
@@ -140,7 +152,9 @@ export class RequestClient implements HttpClient {
accessToken: string | undefined,
contentType: string = 'application/json',
overrideHeaders: { [key: string]: string | number } = {},
debug: boolean = false
debug: boolean = false,
captureRequest: boolean = false,
sasJob: string = ''
): Promise<{ result: T; etag: string; status: number }> {
const headers = {
...this.getHeaders(accessToken, contentType),
@@ -160,8 +174,11 @@ export class RequestClient implements HttpClient {
.get<T>(url, requestConfig)
.then((response) => {
throwIfError(response)
return this.parseResponse<T>(response)
const responseToReturn = this.parseResponse<T>(response)
if (captureRequest) {
this.appendRequest(responseToReturn, sasJob, debug)
}
return responseToReturn
})
.catch(async (e) => {
return await this.handleError(
@@ -182,12 +199,15 @@ export class RequestClient implements HttpClient {
})
}
public async post<T>(
public post<T>(
url: string,
data: any,
accessToken: string | undefined,
contentType = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}
overrideHeaders: { [key: string]: string | number } = {},
debug: boolean = false,
captureRequest: boolean = false,
sasJob: string = ''
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
@@ -198,7 +218,11 @@ export class RequestClient implements HttpClient {
.post<T>(url, data, { headers, withCredentials: true })
.then((response) => {
throwIfError(response)
return this.parseResponse<T>(response)
const responseToReturn = this.parseResponse<T>(response)
if (captureRequest) {
this.appendRequest(responseToReturn, sasJob, debug)
}
return responseToReturn
})
.catch(async (e) => {
return await this.handleError(e, () =>
@@ -281,16 +305,12 @@ export class RequestClient implements HttpClient {
}
try {
const response = await this.httpClient.post(url, content, {
headers,
transformRequest: (requestBody) => requestBody
})
const response = await this.httpClient.post(url, content, { headers })
return {
result: response.data,
etag: response.headers['etag'] as string
}
} catch (e: any) {
} catch (e) {
const response = e.response as AxiosResponse
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetFileUploadCsrfToken(response)
@@ -510,25 +530,6 @@ export class RequestClient implements HttpClient {
return responseToReturn
}
private createHttpClient(baseUrl: string, allowInsecure = false) {
const https = require('https')
if (allowInsecure && https.Agent) {
this.httpClient = axios.create({
baseURL: baseUrl,
httpsAgent: new https.Agent({
rejectUnauthorized: !allowInsecure
})
})
} else {
this.httpClient = axios.create({
baseURL: baseUrl
})
}
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 401
}
}
export const throwIfError = (response: AxiosResponse) => {
@@ -574,60 +575,46 @@ export const throwIfError = (response: AxiosResponse) => {
}
const parseError = (data: string) => {
if (!data) return null
try {
const responseJson = JSON.parse(data?.replace(/[\n\r]/g, ' '))
if (responseJson.errorCode && responseJson.message) {
return new JobExecutionError(
responseJson.errorCode,
responseJson.message,
data?.replace(/[\n\r]/g, ' ')
)
}
} catch (_) {}
try {
const hasError = data?.includes('{"errorCode')
if (hasError) {
const parts = data.split('{"errorCode')
if (parts.length > 1) {
const error = '{"errorCode' + parts[1].split('"}')[0] + '"}'
const errorJson = JSON.parse(error.replace(/[\n\r]/g, ' '))
return new JobExecutionError(
errorJson.errorCode,
errorJson.message,
data?.replace(/[\n\r]/g, '\n')
return responseJson.errorCode && responseJson.message
? new JobExecutionError(
responseJson.errorCode,
responseJson.message,
data?.replace(/[\n\r]/g, ' ')
)
: null
} catch (_) {
try {
const hasError = data?.includes('{"errorCode')
if (hasError) {
const parts = data.split('{"errorCode')
if (parts.length > 1) {
const error = '{"errorCode' + parts[1].split('"}')[0] + '"}'
const errorJson = JSON.parse(error.replace(/[\n\r]/g, ' '))
return new JobExecutionError(
errorJson.errorCode,
errorJson.message,
data?.replace(/[\n\r]/g, '\n')
)
}
return null
}
}
} catch (_) {}
try {
const hasError = !!data?.match(/stored process not found: /i)
if (hasError) {
const parts = data.split(/stored process not found: /i)
if (parts.length > 1) {
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
const message = `Stored process not found: ${storedProcessPath}`
return new JobExecutionError(404, message, '')
try {
const hasError = !!data?.match(/stored process not found: /i)
if (hasError) {
const parts = data.split(/stored process not found: /i)
if (parts.length > 1) {
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
const message = `Stored process not found: ${storedProcessPath}`
return new JobExecutionError(404, message, '')
}
}
} catch (_) {
return null
}
} catch (_) {
return null
}
} catch (_) {}
try {
const hasError =
!!data?.match(/Stored Process Error/i) &&
!!data?.match(/This request completed with errors./i)
if (hasError) {
const parts = data.split('<h2>SAS Log</h2>')
if (parts.length > 1) {
const log = parts[1].split('<pre>')[1].split('</pre>')[0]
const message = `This request completed with errors.`
return new JobExecutionError(404, message, log)
}
}
} catch (_) {}
return null
}
}

View File

@@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
import { FileUploader } from '../job-execution/FileUploader'
import { FileUploader } from '../FileUploader'
import { SASjsConfig, UploadFile } from '../types'
import { RequestClient } from '../request/RequestClient'
import axios from 'axios'
@@ -39,66 +39,56 @@ describe('FileUploader', () => {
}
const fileUploader = new FileUploader(
config.serverUrl,
config.serverType!,
config,
'/jobs/path',
new RequestClient('https://sample.server.com')
)
it('should upload successfully', async () => {
const sasJob = 'test/upload'
const data = prepareFilesAndParams()
const { files, params } = prepareFilesAndParams()
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: sampleResponse })
)
const res = await fileUploader.execute(sasJob, data, config)
const res = await fileUploader.uploadFile(sasJob, files, params)
expect(res).toEqual(JSON.parse(sampleResponse))
})
it('should upload successfully when login is required', async () => {
mockedAxios.post
.mockImplementationOnce(() =>
Promise.resolve({ data: '<form action="Logon">' })
)
.mockImplementationOnce(() => Promise.resolve({ data: sampleResponse }))
const loginCallback = jest.fn().mockImplementation(async () => {
await fileUploader.resendWaitingRequests()
Promise.resolve()
})
const sasJob = 'test'
const data = prepareFilesAndParams()
const res = await fileUploader.execute(sasJob, data, config, loginCallback)
expect(res).toEqual(JSON.parse(sampleResponse))
expect(mockedAxios.post).toHaveBeenCalledTimes(2)
expect(loginCallback).toHaveBeenCalled()
})
it('should an error when no files are provided', async () => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
const res: any = await fileUploader
.execute(sasJob, files, params, config)
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(res.error.message).toEqual('At least one file must be provided.')
expect(err.error.message).toEqual('At least one file must be provided.')
})
it('should throw an error when no sasJob is provided', async () => {
const sasJob = ''
const data = prepareFilesAndParams()
const { files, params } = prepareFilesAndParams()
const res: any = await fileUploader
.execute(sasJob, data, config)
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(res.error.message).toEqual('sasJob must be provided.')
expect(err.error.message).toEqual('sasJob must be provided.')
})
it('should throw an error when login is required', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '<form action="Logon">' })
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('You must be logged in to upload a file.')
})
it('should throw an error when invalid JSON is returned by the server', async () => {
@@ -107,13 +97,12 @@ describe('FileUploader', () => {
)
const sasJob = 'test'
const data = prepareFilesAndParams()
const { files, params } = prepareFilesAndParams()
const res: any = await fileUploader
.execute(sasJob, data, config)
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(res.error.message).toEqual('File upload request failed.')
expect(err.error.message).toEqual('File upload request failed.')
})
it('should throw an error when the server request fails', async () => {
@@ -122,11 +111,11 @@ describe('FileUploader', () => {
)
const sasJob = 'test'
const data = prepareFilesAndParams()
const { files, params } = prepareFilesAndParams()
const res: any = await fileUploader
.execute(sasJob, data, config)
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(res.error.message).toEqual('File upload request failed.')
expect(err.error.message).toEqual('File upload request failed.')
})
})

View File

@@ -1,5 +0,0 @@
export interface ExecutionQuery {
_program: string
_debug?: number
_log?: boolean
}

View File

@@ -1,47 +0,0 @@
export interface FileTree {
members: [FolderMember, ServiceMember]
}
export enum MemberType {
folder = 'folder',
service = 'service'
}
export interface FolderMember {
name: string
type: MemberType.folder
members: [FolderMember, ServiceMember]
}
export interface ServiceMember {
name: string
type: MemberType.service
code: string
}
export const isFileTree = (arg: any): arg is FileTree =>
arg &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isFolderMember = (arg: any): arg is FolderMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.folder &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isServiceMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
arg.code &&
typeof arg.code === 'string'

View File

@@ -1,8 +0,0 @@
export interface LoginOptions {
onLoggedOut?: () => Promise<boolean>
}
export interface LoginResult {
isLoggedIn: boolean
userName: string
}

View File

@@ -59,13 +59,4 @@ export class SASjsConfig {
* Changing this setting is not recommended.
*/
allowInsecureRequests = false
/**
* Supported login mechanisms are - Redirected and Default
*/
loginMechanism: LoginMechanism = LoginMechanism.Default
}
export enum LoginMechanism {
Default = 'Default',
Redirected = 'Redirected'
}

View File

@@ -12,5 +12,3 @@ export * from './Session'
export * from './UploadFile'
export * from './PollOptions'
export * from './WriteStream'
export * from './FileTree'
export * from './ExecuteScript'

View File

@@ -1,22 +0,0 @@
import { ExtraResponseAttributes } from '@sasjs/utils/types'
export async function appendExtraResponseAttributes(
response: any,
extraResponseAttributes: ExtraResponseAttributes[]
) {
let responseObject = {}
if (extraResponseAttributes?.length) {
const extraAttributes = extraResponseAttributes.reduce(
(map: any, obj: any) => ((map[obj] = response[obj]), map),
{}
)
responseObject = {
result: response.result,
...extraAttributes
}
} else responseObject = response.result
return responseObject
}

View File

@@ -1,2 +0,0 @@
export const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -1,7 +1,6 @@
export * from './asyncForEach'
export * from './compareTimestamps'
export * from './convertToCsv'
export * from './delay'
export * from './isNode'
export * from './isRelativePath'
export * from './isUri'
@@ -16,4 +15,3 @@ export * from './parseWeboutResponse'
export * from './fetchLogByChunks'
export * from './getValidJson'
export * from './parseViyaDebugResponse'
export * from './appendExtraResponseAttributes'

View File

@@ -1,177 +0,0 @@
enum domIDs {
styles = 'sasjsAdapterStyles',
overlay = 'sasjsAdapterLoginPromptBG',
dialog = 'sasjsAdapterLoginPrompt'
}
const cssPrefix = 'sasjs-adapter'
const classes = {
popUp: `${cssPrefix}popUp`,
popUpBG: `${cssPrefix}popUpBG`
}
export const openLoginPrompt = (): Promise<boolean> => {
return new Promise(async (resolve) => {
const style = document.createElement('style')
style.id = domIDs.styles
style.innerText = cssContent
const loginPromptBG = document.createElement('div')
loginPromptBG.id = domIDs.overlay
loginPromptBG.classList.add(classes.popUpBG)
const loginPrompt = document.createElement('div')
loginPrompt.id = domIDs.dialog
loginPrompt.classList.add(classes.popUp)
const title = document.createElement('h1')
title.innerText = 'Session Expired!'
loginPrompt.appendChild(title)
const descHolder = document.createElement('div')
const desc = document.createElement('span')
desc.innerText = 'You need to relogin, click OK to login.'
descHolder.appendChild(desc)
loginPrompt.appendChild(descHolder)
const buttonCancel = document.createElement('button')
buttonCancel.classList.add('cancel')
buttonCancel.innerText = 'Cancel'
buttonCancel.onclick = () => {
closeLoginPrompt()
resolve(false)
}
loginPrompt.appendChild(buttonCancel)
const buttonOk = document.createElement('button')
buttonOk.classList.add('confirm')
buttonOk.innerText = 'Ok'
buttonOk.onclick = () => {
closeLoginPrompt()
resolve(true)
}
loginPrompt.appendChild(buttonOk)
document.body.style.overflow = 'hidden'
document.body.appendChild(style)
document.body.appendChild(loginPromptBG)
document.body.appendChild(loginPrompt)
})
}
const closeLoginPrompt = () => {
Object.values(domIDs).forEach((id) => {
const elem = document.getElementById(id)
elem?.parentNode?.removeChild(elem)
})
document.body.style.overflow = 'auto'
}
const cssContent = `
.${classes.popUpBG} ,
.${classes.popUp} {
z-index: 10000;
}
.${classes.popUp} {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
display: block;
position: fixed;
top: 40%;
left: 50%;
padding: 0;
font-size: 14px;
font-family: 'PT Sans', sans-serif;
color: #fff;
border-style: none;
z-index: 999;
overflow: hidden;
background: rgba(0, 0, 0, 0.2);
margin: 0;
width: 100%;
max-width: 300px;
height: auto;
max-height: 300px;
transform: translate(-50%, -50%);
}
.${classes.popUp} > h1 {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
padding: 5px;
min-height: 40px;
font-size: 1.2em;
font-weight: bold;
text-align: center;
color: #fff;
background-color: transparent;
border-style: none;
border-width: 5px;
border-color: black;
}
.${classes.popUp} > div {
width: 100%;
height: calc(100% -108px);
margin: 0;
display: block;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
padding: 5%;
text-align: center;
border-width: 1px;
border-color: #ccc;
border-style: none none solid none;
overflow: auto;
}
.${classes.popUp} > div > span {
display: table-cell;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
margin: 0;
padding: 0;
width: 300px;
height: 108px;
vertical-align: middle;
border-style: none;
}
.${classes.popUp} .cancel {
float: left;
}
.${classes.popUp} .confirm {
float: right;
}
.${classes.popUp} > button {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
margin: 0;
padding: 10px;
width: 50%;
border: 1px none #ccc;
color: #fff;
font-family: inherit;
cursor: pointer;
height: 50px;
background: rgba(1, 1, 1, 0.2);
}
.${classes.popUp} > button:hover {
background: rgba(0, 0, 0, 0.2);
}
.${classes.popUpBG} {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
opacity: 0.95;
z-index: 50;
background-image: radial-gradient(#0378cd, #012036);
}
`

View File

@@ -25,6 +25,6 @@ export const parseSasViyaDebugResponse = async (
}
return requestClient
.get(serverUrl + jsonUrl, undefined, 'text/plain')
.get(serverUrl + jsonUrl, undefined)
.then((res: any) => getValidJson(res.result))
}