1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 17:34:34 +00:00

Compare commits

..

1 Commits

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

View File

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

View File

@@ -13,15 +13,14 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [lts/fermium] node-version: [15.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Check code style - name: Check code style

2
.gitignore vendored
View File

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

View File

@@ -3,4 +3,3 @@ docs/
.github/ .github/
*.md *.md
*.spec.ts *.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: The backend part can be deployed as follows:
```sas ```
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */ %let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */ %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: If you've installed it via NPM, you can import it as a default import like so:
```js ```
import SASjs from '@sasjs/adapter'; import SASjs from '@sasjs/adapter';
``` ```
You can then instantiate it with: You can then instantiate it with:
```js ```
const sasJs = new SASjs({your config}) const sasJs = new SASjs({your config})
``` ```
@@ -119,7 +119,6 @@ sasJs.request("/path/to/my/service", dataObject)
console.log(response.tablewith2cols1row[0].COL1.value) 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: 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 ```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 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: The following snippet shows the process of SAS tables arriving / leaving:
```sas ```sas
/* fetch all input tables sent from frontend - they arrive as work tables */ /* fetch all input tables sent from frontend - they arrive as work tables */
%webout(FETCH) %webout(FETCH)
@@ -163,6 +161,7 @@ run;
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */ %webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
%webout(OBJ,tables,label=newtable) /* rename tables on export */ %webout(OBJ,tables,label=newtable) /* rename tables on export */
%webout(CLOSE) /* close the JSON and send some extra useful variables too */ %webout(CLOSE) /* close the JSON and send some extra useful variables too */
``` ```
## Configuration ## Configuration
@@ -173,7 +172,6 @@ Configuration on the client side involves passing an object on startup, which ca
* `serverType` - either `SAS9` or `SASVIYA`. * `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. * `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
* `debug` - if `true` then SAS Logs and extra debug information is returned. * `debug` - if `true` then SAS Logs and extra debug information is returned.
* `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. * `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`. * `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
@@ -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. This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
```json ```
{ {
appLoc:"/Your/Path", appLoc:"/Your/Path",
serverType:"SASVIYA", 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. With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
```json ```
{ {
appLoc:"/Your/Path", appLoc:"/Your/Path",
serverType:"SASVIYA", serverType:"SASVIYA",
useComputeApi: true, useComputeApi: true,
contextName: "yourComputeContext" contextName: 'yourComputeContext'
} }
``` ```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20050
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,40 +41,36 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/express": "^4.17.13",
"@types/form-data": "^2.5.0", "@types/form-data": "^2.5.0",
"@types/jest": "^27.0.2", "@types/jest": "^27.0.1",
"@types/mime": "^2.0.3", "@types/mime": "^2.0.3",
"@types/pem": "^1.9.6",
"@types/tough-cookie": "^4.0.1", "@types/tough-cookie": "^4.0.1",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"cp": "^0.2.0", "cp": "^0.2.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"express": "^4.17.1", "jest": "^27.1.0",
"jest": "^27.2.0",
"jest-extended": "^0.11.5", "jest-extended": "^0.11.5",
"node-polyfill-webpack-plugin": "^1.1.4", "node-polyfill-webpack-plugin": "^1.1.4",
"path": "^0.12.7", "path": "^0.12.7",
"pem": "^1.14.4",
"process": "^0.11.10", "process": "^0.11.10",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semantic-release": "^18.0.0", "semantic-release": "^17.4.7",
"terser-webpack-plugin": "^5.2.4", "terser-webpack-plugin": "^5.2.0",
"ts-jest": "^27.0.3", "ts-jest": "^27.0.3",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.2",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "0.19.2", "typedoc": "^0.21.9",
"typedoc-neo-theme": "^1.1.1", "typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6", "typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "4.3.5", "typescript": "4.3.5",
"webpack": "^5.56.0", "webpack": "^5.44.0",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "^2.32.0", "@sasjs/utils": "^2.30.0",
"axios": "^0.21.4", "axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1", "axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"https": "^1.0.0", "https": "^1.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz", "@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/jest": "^26.0.20",
"@types/node": "^14.14.41", "@types/node": "^14.14.41",
"@types/react": "^17.0.1", "@types/react": "^17.0.1",
@@ -23,7 +23,7 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz", "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:tests-win": "scp %DEPLOY_PATH% ./build/*",
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests" "deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
}, },

View File

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

View File

@@ -14,16 +14,16 @@ const App = (): ReactElement<{}> => {
useEffect(() => { useEffect(() => {
if (adapter) { if (adapter) {
const testSuites = [ const testSuites = [
basicTests(adapter, config.userName, config.password), // basicTests(adapter, config.userName, config.password),
sendArrTests(adapter), // sendArrTests(adapter),
sendObjTests(adapter), // sendObjTests(adapter),
specialCaseTests(adapter), // specialCaseTests(adapter),
sasjsRequestTests(adapter) sasjsRequestTests(adapter)
] ]
if (adapter.getSasjsConfig().serverType === 'SASVIYA') { // if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
testSuites.push(computeTests(adapter)) // testSuites.push(computeTests(adapter))
} // }
setTestSuites(testSuites) 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 { TestSuite } from '@sasjs/test-framework'
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
@@ -13,7 +13,7 @@ const defaultConfig: SASjsConfig = {
debug: false, debug: false,
contextName: 'SAS Job Execution compute context', contextName: 'SAS Job Execution compute context',
useComputeApi: false, useComputeApi: false,
loginMechanism: LoginMechanism.Default allowInsecureRequests: false
} }
const customConfig = { const customConfig = {
@@ -41,19 +41,6 @@ export const basicTests = (
assertion: (response: any) => assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName 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', title: 'Multiple Log in attempts',
description: description:
@@ -61,7 +48,7 @@ export const basicTests = (
test: async () => { test: async () => {
await adapter.logOut() await adapter.logOut()
await adapter.logIn('invalid', 'invalid') await adapter.logIn('invalid', 'invalid')
return await adapter.logIn(userName, password) return adapter.logIn(userName, password)
}, },
assertion: (response: any) => assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName response && response.isLoggedIn && response.userName === userName
@@ -77,8 +64,8 @@ export const basicTests = (
'common/sendArr', 'common/sendArr',
stringData, stringData,
undefined, undefined,
async () => { () => {
await adapter.logIn(userName, password) adapter.logIn(userName, password)
} }
) )
}, },
@@ -164,7 +151,7 @@ export const basicTests = (
description: description:
'Should complete successful request with extra attributes present in response', 'Should complete successful request with extra attributes present in response',
test: async () => { test: async () => {
const config: Partial<SASjsConfig> = { const config = {
useComputeApi: false 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

@@ -1,4 +1,3 @@
import * as https from 'https'
import { generateTimestamp } from '@sasjs/utils/time' import { generateTimestamp } from '@sasjs/utils/time'
import * as NodeFormData from 'form-data' import * as NodeFormData from 'form-data'
import { Sas9RequestClient } from './request/Sas9RequestClient' import { Sas9RequestClient } from './request/Sas9RequestClient'
@@ -14,10 +13,10 @@ export class SAS9ApiClient {
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private jobsPath: string, private jobsPath: string,
httpsAgentOptions?: https.AgentOptions allowInsecureRequests: boolean
) { ) {
if (serverUrl) isUrl(serverUrl) if (serverUrl) isUrl(serverUrl)
this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions) this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
} }
/** /**

View File

@@ -51,16 +51,6 @@ export class SASViyaApiClient {
) )
private folderMap = new Map<string, Job[]>() 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() { public get debug() {
return this._debug return this._debug
} }
@@ -792,12 +782,24 @@ export class SASViyaApiClient {
jobResult = await this.requestClient.get<any>( jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`, `${this.serverUrl}${resultLink}/content`,
access_token, access_token,
'text/plain' 'text/plain',
{},
debug,
true,
sasJob
) )
} }
if (debug && logLink) { if (debug && logLink) {
log = await this.requestClient 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')) .then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
} }
if (jobStatus === 'failed') { if (jobStatus === 'failed') {

View File

@@ -1,17 +1,8 @@
import { compareTimestamps, asyncForEach } from './utils' import { compareTimestamps, asyncForEach } from './utils'
import { import { SASjsConfig, UploadFile, EditContextInput, PollOptions } from './types'
SASjsConfig,
UploadFile,
EditContextInput,
PollOptions,
LoginMechanism,
FolderMember,
ServiceMember,
ExecutionQuery
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient' import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
import { SASjsApiClient } from './SASjsApiClient' import { FileUploader } from './FileUploader'
import { AuthManager } from './auth' import { AuthManager } from './auth'
import { import {
ServerType, ServerType,
@@ -25,11 +16,9 @@ import {
WebJobExecutor, WebJobExecutor,
ComputeJobExecutor, ComputeJobExecutor,
JesJobExecutor, JesJobExecutor,
Sas9JobExecutor, Sas9JobExecutor
FileUploader
} from './job-execution' } from './job-execution'
import { ErrorResponse } from './types/errors' import { ErrorResponse } from './types/errors'
import { LoginOptions, LoginResult } from './types/Login'
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: '', serverUrl: '',
@@ -40,7 +29,7 @@ const defaultConfig: SASjsConfig = {
debug: false, debug: false,
contextName: 'SAS Job Execution compute context', contextName: 'SAS Job Execution compute context',
useComputeApi: null, useComputeApi: null,
loginMechanism: LoginMechanism.Default allowInsecureRequests: false
} }
/** /**
@@ -52,7 +41,6 @@ export default class SASjs {
private jobsPath: string = '' private jobsPath: string = ''
private sasViyaApiClient: SASViyaApiClient | null = null private sasViyaApiClient: SASViyaApiClient | null = null
private sas9ApiClient: SAS9ApiClient | null = null private sas9ApiClient: SAS9ApiClient | null = null
private SASjsApiClient: SASjsApiClient | null = null
private fileUploader: FileUploader | null = null private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null private requestClient: RequestClient | null = null
@@ -61,7 +49,8 @@ export default class SASjs {
private jesJobExecutor: JobExecutor | null = null private jesJobExecutor: JobExecutor | null = null
private sas9JobExecutor: JobExecutor | null = null private sas9JobExecutor: JobExecutor | null = null
constructor(config?: Partial<SASjsConfig>) { constructor(config?: any) {
console.log('from SASjs constructor')
this.sasjsConfig = { this.sasjsConfig = {
...defaultConfig, ...defaultConfig,
...config ...config
@@ -511,7 +500,7 @@ export default class SASjs {
...this.sasjsConfig, ...this.sasjsConfig,
...config ...config
} }
this.setupConfiguration() await this.setupConfiguration()
} }
/** /**
@@ -538,27 +527,8 @@ export default class SASjs {
* @param username - a string representing the username. * @param username - a string representing the username.
* @param password - a string representing the password. * @param password - a string representing the password.
*/ */
public async logIn( public async logIn(username: string, password: string) {
username?: string, return this.authManager!.logIn(username, password)
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)
} }
/** /**
@@ -575,32 +545,24 @@ export default class SASjs {
* Process). Is prepended at runtime with the value of `appLoc`. * 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 files - array of files to be uploaded, including File object and file name.
* @param params - request URL parameters. * @param params - request URL parameters.
* @param config - provide any changes to the config here, for instance to * @param overrideSasjsConfig - object to override existing config (optional)
* 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.
*/ */
public async uploadFile( public uploadFile(
sasJob: string, sasJob: string,
files: UploadFile[], files: UploadFile[],
params: { [key: string]: any } | null, params: any,
config: { [key: string]: any } = {}, overrideSasjsConfig?: any
loginRequiredCallback?: () => any
) { ) {
config = { const fileUploader = overrideSasjsConfig
...this.sasjsConfig, ? new FileUploader(
...config { ...this.sasjsConfig, ...overrideSasjsConfig },
} this.jobsPath,
const data = { files, params } this.requestClient!
)
: this.fileUploader ||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
return await this.fileUploader!.execute( return fileUploader.uploadFile(sasJob, files, params)
sasJob,
data,
config,
loginRequiredCallback
)
} }
/** /**
@@ -650,6 +612,7 @@ export default class SASjs {
config.useComputeApi !== null config.useComputeApi !== null
) { ) {
if (config.useComputeApi) { if (config.useComputeApi) {
console.log(615)
return await this.computeJobExecutor!.execute( return await this.computeJobExecutor!.execute(
sasJob, sasJob,
data, data,
@@ -796,7 +759,7 @@ export default class SASjs {
sasApiClient = new SAS9ApiClient( sasApiClient = new SAS9ApiClient(
serverUrl, serverUrl,
this.jobsPath, this.jobsPath,
this.sasjsConfig.httpsAgentOptions this.sasjsConfig.allowInsecureRequests
) )
} }
} else { } else {
@@ -828,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. * Kicks off execution of the given job via the compute API.
* @returns an object representing the compute session created for the given job. * @returns an object representing the compute session created for the given job.
@@ -894,7 +849,6 @@ export default class SASjs {
await this.webJobExecutor?.resendWaitingRequests() await this.webJobExecutor?.resendWaitingRequests()
await this.computeJobExecutor?.resendWaitingRequests() await this.computeJobExecutor?.resendWaitingRequests()
await this.jesJobExecutor?.resendWaitingRequests() await this.jesJobExecutor?.resendWaitingRequests()
await this.fileUploader?.resendWaitingRequests()
} }
/** /**
@@ -931,8 +885,10 @@ export default class SASjs {
* @returns SASjsRequest[] * @returns SASjsRequest[]
*/ */
public getSasRequests() { public getSasRequests() {
const requests = [...this.requestClient!.getRequests()] console.log('from getSASRequests')
const requests = this.requestClient!.getRequests()
const sortedRequests = requests.sort(compareTimestamps) const sortedRequests = requests.sort(compareTimestamps)
console.log('sortedRequests', sortedRequests)
return sortedRequests return sortedRequests
} }
@@ -960,17 +916,10 @@ export default class SASjs {
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1) this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
} }
if (!this.requestClient) { this.requestClient = new RequestClient(
this.requestClient = new RequestClient( this.sasjsConfig.serverUrl,
this.sasjsConfig.serverUrl, this.sasjsConfig.allowInsecureRequests
this.sasjsConfig.httpsAgentOptions )
)
} else {
this.requestClient.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions
)
}
this.jobsPath = this.jobsPath =
this.sasjsConfig.serverType === ServerType.SasViya this.sasjsConfig.serverType === ServerType.SasViya
@@ -985,49 +934,34 @@ export default class SASjs {
) )
if (this.sasjsConfig.serverType === ServerType.SasViya) { if (this.sasjsConfig.serverType === ServerType.SasViya) {
if (this.sasViyaApiClient) { if (this.sasViyaApiClient)
this.sasViyaApiClient!.setConfig( this.sasViyaApiClient!.setConfig(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc this.sasjsConfig.appLoc
) )
} else { else
this.sasViyaApiClient = new SASViyaApiClient( this.sasViyaApiClient = new SASViyaApiClient(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc, this.sasjsConfig.appLoc,
this.sasjsConfig.contextName, this.sasjsConfig.contextName,
this.requestClient this.requestClient
) )
}
this.sasViyaApiClient.debug = this.sasjsConfig.debug this.sasViyaApiClient.debug = this.sasjsConfig.debug
} }
if (this.sasjsConfig.serverType === ServerType.Sas9) { if (this.sasjsConfig.serverType === ServerType.Sas9) {
if (this.sas9ApiClient) { if (this.sas9ApiClient)
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl) this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
} else { else
this.sas9ApiClient = new SAS9ApiClient( this.sas9ApiClient = new SAS9ApiClient(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.jobsPath, this.jobsPath,
this.sasjsConfig.httpsAgentOptions 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.fileUploader = new FileUploader(
this.sasjsConfig.serverUrl, this.sasjsConfig,
this.sasjsConfig.serverType!,
this.jobsPath, this.jobsPath,
this.requestClient this.requestClient
) )
@@ -1044,8 +978,7 @@ export default class SASjs {
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!, this.sasjsConfig.serverType!,
this.jobsPath, this.jobsPath,
this.requestClient, this.sasjsConfig.allowInsecureRequests
this.sasjsConfig.httpsAgentOptions
) )
this.computeJobExecutor = new ComputeJobExecutor( this.computeJobExecutor = new ComputeJobExecutor(

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` const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
jobResult = await requestClient jobResult = await requestClient
.get<any>(resultLink, access_token, 'text/plain') .get<any>(
resultLink,
access_token,
'text/plain',
{},
debug,
true,
jobPath
)
.catch(async (e) => { .catch(async (e) => {
if (e instanceof NotFoundError) { if (e instanceof NotFoundError) {
if (logLink) { if (logLink) {

View File

@@ -4,7 +4,7 @@ import { getTokens } from '../../auth/getTokens'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { JobStatePollError } from '../../types/errors' import { JobStatePollError } from '../../types/errors'
import { Link, WriteStream } from '../../types' import { Link, WriteStream } from '../../types'
import { delay, isNode } from '../../utils' import { isNode } from '../../utils'
export async function pollJobState( export async function pollJobState(
requestClient: RequestClient, requestClient: RequestClient,
@@ -246,3 +246,5 @@ const doPoll = async (
return { state, pollCount } 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 { ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { LoginOptions, LoginResult } from '../types/Login'
import { serialize } from '../utils' import { serialize } from '../utils'
import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin'
export class AuthManager { export class AuthManager {
public userName = '' public userName = ''
private loginUrl: string private loginUrl: string
private logoutUrl: string private logoutUrl: string
private redirectedLoginUrl = `/SASLogon/home`
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private serverType: ServerType, private serverType: ServerType,
@@ -24,137 +19,65 @@ export class AuthManager {
: '/SASLogon/logout.do?' : '/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. * Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username. * @param username - a string representing the username.
* @param password - a string representing the password. * @param password - a string representing the password.
* @returns - a boolean `isLoggedin` and a string `username`
*/ */
public async logIn(username: string, password: string): Promise<LoginResult> { public async logIn(username: string, password: string) {
const loginParams = { const loginParams: any = {
_service: 'default', _service: 'default',
username, username,
password password
} }
let { this.userName = loginParams.username
isLoggedIn: isLoggedInAlready,
loginForm,
userName: currentSessionUsername
} = await this.checkSession()
if (isLoggedInAlready) { const { isLoggedIn, loginForm } = await this.checkSession()
if (currentSessionUsername === loginParams.username) {
await this.loginCallback()
this.userName = currentSessionUsername! if (isLoggedIn) {
return { await this.loginCallback()
isLoggedIn: true,
userName: this.userName return {
} isLoggedIn,
} else { userName: this.userName
await this.logOut()
loginForm = await this.getNewLoginForm()
} }
} else this.userName = '' }
let loginResponse = await this.sendLoginRequest(loginForm, loginParams) let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let isLoggedIn = isLogInSuccess(loginResponse) let loggedIn = isLogInSuccess(loginResponse)
if (!isLoggedIn) { if (!loggedIn) {
if (isCredentialsVerifyError(loginResponse)) { if (isCredentialsVerifyError(loginResponse)) {
const newLoginForm = await this.getLoginForm(loginResponse) const newLoginForm = await this.getLoginForm(loginResponse)
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams) loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
} }
const res = await this.checkSession() const currentSession = await this.checkSession()
isLoggedIn = res.isLoggedIn loggedIn = currentSession.isLoggedIn
if (isLoggedIn) this.userName = res.userName
} else {
this.userName = loginParams.username
} }
if (isLoggedIn) { if (loggedIn) {
if (this.serverType === ServerType.Sas9) { 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() this.loginCallback()
} else this.userName = '' }
return { return {
isLoggedIn, isLoggedIn: !!loggedIn,
userName: this.userName 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( private async sendLoginRequest(
loginForm: { [key: string]: any }, loginForm: { [key: string]: any },
loginParams: { [key: string]: any } loginParams: { [key: string]: any }
@@ -180,53 +103,14 @@ export class AuthManager {
/** /**
* Checks whether a session is active, or login is required. * Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing three values * @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
* - a boolean `isLoggedIn`
* - a string `userName` and
* - a form `loginForm` if not loggedin.
*/ */
public async checkSession(): Promise<{ public async checkSession() {
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
}> {
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution. //For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
//For SAS9 we will send request on SASStoredProcess //For SAS9 we will send request on SASStoredProcess
const url = const url =
this.serverType === ServerType.SasViya this.serverType === 'SASVIYA'
? `${this.serverUrl}/identities/users/@currentUser` ? `${this.serverUrl}/identities`
: `${this.serverUrl}/SASStoredProcess` : `${this.serverUrl}/SASStoredProcess`
const { result: loginResponse } = await this.requestClient const { result: loginResponse } = await this.requestClient
@@ -236,31 +120,27 @@ export class AuthManager {
}) })
const isLoggedIn = loginResponse !== 'authErr' 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 => { const { result: formResponse } = await this.requestClient.get<string>(
switch (this.serverType) { this.loginUrl.replace('.do', ''),
case ServerType.SasViya: undefined,
return response?.id 'text/plain'
)
case ServerType.Sas9: loginForm = await this.getLoginForm(formResponse)
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:
console.error('Server Type not found in extractUserName function')
return ''
} }
return Promise.resolve({
isLoggedIn,
userName: this.userName,
loginForm
})
} }
private getLoginForm(response: any) { private getLoginForm(response: any) {

View File

@@ -46,7 +46,7 @@ export async function getAccessToken(
) )
.then((res) => res.result as SasAuthResponse) .then((res) => res.result as SasAuthResponse)
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while getting access token. ') throw prefixMessage(err, 'Error while getting access token')
}) })
return authResponse return authResponse

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 { ServerType } from '@sasjs/utils/types'
import axios from 'axios' import axios from 'axios'
import { import {
mockedCurrentUserApi,
mockLoginAuthoriseRequiredResponse, mockLoginAuthoriseRequiredResponse,
mockLoginSuccessResponse mockLoginSuccessResponse
} from './mockResponses' } from './mockResponses'
import { serialize } from '../../utils' import { serialize } from '../../utils'
import * as openWebPageModule from '../openWebPage'
import * as verifySasViyaLoginModule from '../verifySasViyaLogin'
import * as verifySas9LoginModule from '../verifySas9Login'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
jest.mock('axios') jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios> const mockedAxios = axios as jest.Mocked<typeof axios>
@@ -61,614 +57,134 @@ describe('AuthManager', () => {
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?') 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 () => {
it('should call the auth callback and return when already logged in', async () => { const authManager = new AuthManager(
const authManager = new AuthManager( serverUrl,
serverUrl, serverType,
serverType, requestClient,
requestClient, authCallback
authCallback )
) jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
jest.spyOn(authManager, 'checkSession').mockImplementation(() => Promise.resolve({
Promise.resolve({ isLoggedIn: true,
isLoggedIn: true, userName: 'test',
userName, loginForm: 'test'
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'
}) })
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 loginResponse = await authManager.logIn(userName, password)
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) expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(loginResponse.isLoggedIn).toBeTruthy() expect(authCallback).toHaveBeenCalledTimes(1)
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
)
})
}) })
describe('login - redirect mechanism', () => { it('should post a login request to the server if not logged in', async () => {
beforeAll(() => { const authManager = new AuthManager(
jest.mock('../openWebPage') serverUrl,
jest serverType,
.spyOn(openWebPageModule, 'openWebPage') requestClient,
.mockImplementation(() => authCallback
Promise.resolve({ close: jest.fn() } as unknown as Window) )
) jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
jest.mock('../verifySasViyaLogin') Promise.resolve({
jest isLoggedIn: false,
.spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin') userName: 'test',
.mockImplementation(() => Promise.resolve({ isLoggedIn: true })) loginForm: { name: 'test' }
jest.mock('../verifySas9Login') })
jest )
.spyOn(verifySas9LoginModule, 'verifySas9Login') mockedAxios.post.mockImplementation(() =>
.mockImplementation(() => Promise.resolve({ isLoggedIn: true })) Promise.resolve({ data: mockLoginSuccessResponse })
}) )
it('should call the auth callback and return when already logged in', async () => { const loginResponse = await authManager.logIn(userName, password)
const authManager = new AuthManager(
serverUrl, expect(loginResponse.isLoggedIn).toBeTruthy()
serverType, expect(loginResponse.userName).toEqual(userName)
requestClient,
authCallback const loginParams = serialize({
) _service: 'default',
jest username: userName,
.spyOn<any, any>(authManager, 'fetchUserName') password,
.mockImplementation(() => name: 'test'
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)
}) })
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('should parse and submit the authorisation form when necessary', async () => {
it('return session information when logged in', async () => { const authManager = new AuthManager(
const authManager = new AuthManager( serverUrl,
serverUrl, serverType,
serverType, requestClient,
requestClient, authCallback
authCallback )
) jest
mockedAxios.get.mockImplementation(() => .spyOn(requestClient, 'authorize')
Promise.resolve({ data: mockedCurrentUserApi(userName) }) .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() mockedAxios.get.mockImplementationOnce(() =>
expect(response.isLoggedIn).toBeTruthy() Promise.resolve({
expect(response.userName).toEqual(userName) data: mockLoginAuthoriseRequiredResponse
expect(mockedAxios.get).toHaveBeenNthCalledWith( })
1, )
`http://test-server.com/identities/users/@currentUser`,
{ await authManager.logIn(userName, password)
withCredentials: true,
responseType: 'text', expect(requestClient.authorize).toHaveBeenCalledWith(
transformResponse: undefined, mockLoginAuthoriseRequiredResponse
headers: { )
Accept: '*/*', })
'Content-Type': 'text/plain'
} 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'
}

Some files were not shown because too many files have changed in this diff Show More