mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c5ada6fa1 | ||
|
|
0f5702e21b | ||
|
|
f10d6ec80a | ||
|
|
490215df90 | ||
|
|
9d27451813 | ||
|
|
a977f59675 | ||
|
|
47be5e77e5 | ||
|
|
d517e79ec0 | ||
|
|
8714ecdde8 | ||
|
|
2cffc57209 | ||
|
|
4e43687de2 | ||
|
|
f8dab83e37 | ||
|
|
655af03cf3 | ||
|
|
0a4dd00edb | ||
|
|
55b4929c54 | ||
|
|
8eb73a6b3c | ||
|
|
0aeb201625 | ||
|
|
e0140a23c2 | ||
|
|
ff6698a9d1 | ||
|
|
843d498b72 | ||
|
|
349612a065 | ||
|
|
e48b22128d | ||
|
|
14dfe4ec51 | ||
|
|
f679b17cbe | ||
|
|
36a0f0e743 | ||
|
|
ad563b9bc8 | ||
|
|
e0051bf276 | ||
|
|
7b72998e1c | ||
|
|
893cce7f21 | ||
|
|
bf7e8fd0e6 | ||
|
|
66d02cf1d1 | ||
|
|
f2c8e40430 | ||
|
|
4b28ee8e73 | ||
|
|
c7e54cfe9f | ||
|
|
50be3acc78 | ||
|
|
66c12a69bb | ||
|
|
e2058f0c71 | ||
|
|
75a74152a5 | ||
|
|
9344c4fd35 | ||
|
|
be0fe00076 | ||
|
|
a2069ee48b | ||
|
|
907885cf85 | ||
|
|
f10ed3236e | ||
|
|
a852a0af7c | ||
|
|
03f3550774 | ||
|
|
2e8d06f9e1 | ||
|
|
1d31ec4dcc | ||
|
|
4407ed68ae | ||
|
|
b3359f2138 | ||
|
|
cff9104adf | ||
|
|
b73f821c19 | ||
|
|
dd3c1a7375 | ||
|
|
c10b5368af | ||
|
|
2df66765c0 | ||
|
|
ed65545571 | ||
|
|
b4ae486520 | ||
|
|
bb7b3d0b84 | ||
|
|
65b34aa015 | ||
|
|
b3c9303def | ||
|
|
19dfda9b6a | ||
|
|
ce2abb448f | ||
|
|
413c3b8098 | ||
|
|
269514a44f | ||
|
|
5fc334dd8b | ||
|
|
1b251f1cea | ||
|
|
70d3e25c7f | ||
|
|
5e9b33e346 | ||
|
|
eb52ec7532 | ||
|
|
f46c4bf3ca | ||
|
|
d8176912cf | ||
|
|
4c54ade2d3 | ||
|
|
52c41dfb3a | ||
|
|
36f0aa7411 | ||
|
|
a10e4ec264 | ||
|
|
851b6bc273 | ||
|
|
23f8d31b1b | ||
|
|
a5683bcd07 | ||
|
|
c7be71c781 | ||
|
|
594f274323 | ||
|
|
ba1ed5e732 | ||
|
|
613cc6b9ef | ||
|
|
8edb00f869 | ||
|
|
60a1f84604 | ||
|
|
36cfaee5db | ||
|
|
cb607c93ca | ||
|
|
03b5e1d824 | ||
|
|
2ea49a425f | ||
|
|
3c894c4147 | ||
|
|
23d151c919 | ||
|
|
6d1c4ff81a | ||
|
|
7a6e6e8333 | ||
|
|
0eba6bdcf4 | ||
|
|
d7ecaf5932 | ||
|
|
e0d85f458b | ||
|
|
3a9cd46e6e | ||
|
|
301edab8ad | ||
|
|
aed39c2ec4 | ||
|
|
cc594eca52 | ||
|
|
3fbca2835e | ||
|
|
61ed5c4fa7 | ||
|
|
e31774ae9d | ||
|
|
00f09179a8 | ||
|
|
4196901e01 | ||
|
|
bf35dd072a | ||
|
|
75e3fd018d | ||
|
|
965dfff7c6 | ||
|
|
ff64dd22ad | ||
|
|
e7cceab065 | ||
|
|
f789b8f7a2 | ||
|
|
9b6ba3548f | ||
|
|
efa4c71b8a | ||
|
|
ecfc1a4bf8 | ||
|
|
246b855c76 | ||
|
|
80aa848617 | ||
|
|
acf045965e | ||
|
|
5a7f64dc35 | ||
|
|
de447b0727 | ||
|
|
a2906abf71 | ||
|
|
ace16efd93 | ||
|
|
13ae5ae756 | ||
|
|
f1b035032f | ||
|
|
1555afe771 | ||
|
|
a2832f1e1a | ||
|
|
56f34508fa |
2
.github/workflows/npmpublish.yml
vendored
2
.github/workflows/npmpublish.yml
vendored
@@ -25,3 +25,5 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Send Slack message
|
||||
run: curl -X POST --data-urlencode "payload={\"channel\":\"#sasjs\", \"username\":\"GitHub CI\", \"text\":\"New version of @sasjs/adapter has been released! \n Please deploy and run `dctests` with new adapter to make sure everything is still in place.\", \"icon_emoji\":\":rocket:\"}" ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,4 +3,6 @@ build
|
||||
|
||||
.env
|
||||
|
||||
/coverage
|
||||
/coverage
|
||||
|
||||
.DS_Store
|
||||
13
cypress.json
13
cypress.json
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"defaultCommandTimeout": 10000,
|
||||
"chromeWebSecurity": false,
|
||||
"screenshotOnRunFailure": false,
|
||||
"env": {
|
||||
"serverUrl": "",
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SAS9",
|
||||
"debug": false,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
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
328
docs/classes/auth.authmanager.html
Normal file
328
docs/classes/auth.authmanager.html
Normal file
File diff suppressed because one or more lines are too long
432
docs/classes/job_execution.basejobexecutor.html
Normal file
432
docs/classes/job_execution.basejobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
435
docs/classes/job_execution.computejobexecutor.html
Normal file
435
docs/classes/job_execution.computejobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
435
docs/classes/job_execution.jesjobexecutor.html
Normal file
435
docs/classes/job_execution.jesjobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
441
docs/classes/job_execution.webjobexecutor.html
Normal file
441
docs/classes/job_execution.webjobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-817.reflection-220.fileuploader.html
Normal file
231
docs/classes/reflection-817.reflection-220.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-817.reflection-220.sas9apiclient.html
Normal file
312
docs/classes/reflection-817.reflection-220.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1715
docs/classes/reflection-817.reflection-220.sasjs.html
Normal file
1715
docs/classes/reflection-817.reflection-220.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1516
docs/classes/reflection-817.reflection-220.sasviyaapiclient.html
Normal file
1516
docs/classes/reflection-817.reflection-220.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-817.reflection-220.sessionmanager.html
Normal file
323
docs/classes/reflection-817.reflection-220.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-830.reflection-222.fileuploader.html
Normal file
231
docs/classes/reflection-830.reflection-222.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-830.reflection-222.sas9apiclient.html
Normal file
312
docs/classes/reflection-830.reflection-222.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1721
docs/classes/reflection-830.reflection-222.sasjs.html
Normal file
1721
docs/classes/reflection-830.reflection-222.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1528
docs/classes/reflection-830.reflection-222.sasviyaapiclient.html
Normal file
1528
docs/classes/reflection-830.reflection-222.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
360
docs/classes/reflection-830.reflection-222.sessionmanager.html
Normal file
360
docs/classes/reflection-830.reflection-222.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
548
docs/classes/request.requestclient.html
Normal file
548
docs/classes/request.requestclient.html
Normal file
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
328
docs/classes/types.authorizeerror.html
Normal file
328
docs/classes/types.authorizeerror.html
Normal file
File diff suppressed because one or more lines are too long
346
docs/classes/types.computejobexecutionerror.html
Normal file
346
docs/classes/types.computejobexecutionerror.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
367
docs/classes/types.jobexecutionerror.html
Normal file
367
docs/classes/types.jobexecutionerror.html
Normal file
File diff suppressed because one or more lines are too long
301
docs/classes/types.loginrequirederror.html
Normal file
301
docs/classes/types.loginrequirederror.html
Normal file
File diff suppressed because one or more lines are too long
325
docs/classes/types.notfounderror.html
Normal file
325
docs/classes/types.notfounderror.html
Normal file
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
302
docs/interfaces/job_execution.jobexecutor.html
Normal file
302
docs/interfaces/job_execution.jobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
392
docs/interfaces/request.httpclient.html
Normal file
392
docs/interfaces/request.httpclient.html
Normal file
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
94
docs/modules/__mocks__.html
Normal file
94
docs/modules/__mocks__.html
Normal file
File diff suppressed because one or more lines are too long
179
docs/modules/auth.html
Normal file
179
docs/modules/auth.html
Normal file
File diff suppressed because one or more lines are too long
144
docs/modules/auth_spec.html
Normal file
144
docs/modules/auth_spec.html
Normal file
File diff suppressed because one or more lines are too long
184
docs/modules/file.html
Normal file
184
docs/modules/file.html
Normal file
File diff suppressed because one or more lines are too long
171
docs/modules/job_execution.html
Normal file
171
docs/modules/job_execution.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-817.html
Normal file
106
docs/modules/reflection-817.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-817.reflection-220.html
Normal file
128
docs/modules/reflection-817.reflection-220.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-830.html
Normal file
106
docs/modules/reflection-830.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-830.reflection-222.html
Normal file
128
docs/modules/reflection-830.reflection-222.html
Normal file
File diff suppressed because one or more lines are too long
118
docs/modules/request.html
Normal file
118
docs/modules/request.html
Normal file
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
188
jest.config.js
188
jest.config.js
@@ -1,10 +1,184 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.+(ts|tsx|js)",
|
||||
"**/?(*.)+(spec|test).+(ts|tsx|js)"
|
||||
],
|
||||
testTimeout: 90000,
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 1,
|
||||
|
||||
// Respect "browser" field in package.json when resolving modules
|
||||
// browser: false,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/7y/nmqg1srj29q6210rs9dfsdzc0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: 'ts-jest/presets/js-with-ts',
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ['**/*spec.[j|t]s?(x)'],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
testPathIgnorePatterns: ['/node_modules/', '/build'],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
'^.+\\.ts?$': 'ts-jest'
|
||||
}
|
||||
};
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// '**/test/**/*.ts?(x)',
|
||||
// '**/?(*.)+(spec|test).ts?(x)'
|
||||
// ]
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
}
|
||||
|
||||
20595
package-lock.json
generated
20595
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -7,7 +7,7 @@
|
||||
"publish:lib": "npm run build && cd build && npm publish",
|
||||
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
"lint": "npx prettier --check 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
"test": "jest --coverage",
|
||||
"test": "jest --silent --coverage",
|
||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||
"postpublish": "git clean -fd",
|
||||
"semantic-release": "semantic-release",
|
||||
@@ -36,32 +36,31 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/isomorphic-fetch": "0.0.35",
|
||||
"@types/jest": "^26.0.20",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^26.6.3",
|
||||
"jest-extended": "^0.11.5",
|
||||
"path": "^0.12.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.3.1",
|
||||
"semantic-release": "^17.3.9",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^8.0.14",
|
||||
"ts-loader": "^8.0.17",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.19.2",
|
||||
"typedoc-neo-theme": "^1.0.10",
|
||||
"typedoc-neo-theme": "^1.1.0",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^5.13.0",
|
||||
"webpack-cli": "^4.3.1"
|
||||
"typescript": "^3.9.9",
|
||||
"webpack": "^5.21.2",
|
||||
"webpack-cli": "^4.5.0"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.0.2",
|
||||
"es6-promise": "^4.2.8",
|
||||
"@sasjs/utils": "^2.5.0",
|
||||
"axios": "^0.21.1",
|
||||
"form-data": "^3.0.0",
|
||||
"https": "^1.0.0",
|
||||
"isomorphic-fetch": "^2.2.1"
|
||||
"https": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
12414
sasjs-tests/package-lock.json
generated
12414
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,21 +4,18 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^1.18.2",
|
||||
"@sasjs/adapter": "^2.2.4",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"@types/jest": "^26.0.3",
|
||||
"@types/node": "^14.0.14",
|
||||
"@types/react": "^16.9.41",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.25",
|
||||
"@types/react": "^17.0.1",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"typescript": "^3.9.6"
|
||||
"react-scripts": "^4.0.2",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -45,6 +42,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.14.1"
|
||||
"node-sass": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SASVIYA",
|
||||
"debug": false,
|
||||
"contextName": "SharedCompute",
|
||||
"contextName": "sasjs adapter compute context",
|
||||
"useComputeApi": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter";
|
||||
import SASjs, { SASjsConfig } from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import { ServerType } from "@sasjs/utils/types";
|
||||
|
||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.SASViya,
|
||||
pathSAS9: "/SASStoredProcess/do",
|
||||
pathSASViya: "/SASJobExecution",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SasViya,
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: false
|
||||
contextName: "SAS Job Execution compute context",
|
||||
useComputeApi: false,
|
||||
allowInsecureRequests: false
|
||||
};
|
||||
|
||||
const customConfig = {
|
||||
@@ -17,7 +21,7 @@ const customConfig = {
|
||||
pathSAS9: "sas9",
|
||||
pathSASViya: "viya",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SAS9,
|
||||
serverType: ServerType.Sas9,
|
||||
debug: false
|
||||
};
|
||||
|
||||
@@ -39,15 +43,46 @@ export const basicTests = (
|
||||
},
|
||||
{
|
||||
title: "Multiple Log in attempts",
|
||||
description: "Should fail on first attempt and should log the user in on second attempt",
|
||||
description:
|
||||
"Should fail on first attempt and should log the user in on second attempt",
|
||||
test: async () => {
|
||||
await adapter.logOut()
|
||||
await adapter.logIn('invalid', 'invalid')
|
||||
return adapter.logIn(userName, password)
|
||||
await adapter.logOut();
|
||||
await adapter.logIn("invalid", "invalid");
|
||||
return adapter.logIn(userName, password);
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: "Trigger login callback",
|
||||
description:
|
||||
"Should trigger required login callback and after successful login, it should finish the request",
|
||||
test: async () => {
|
||||
await adapter.logOut();
|
||||
|
||||
return await adapter.request("common/sendArr", stringData, null, () => {
|
||||
adapter.logIn(userName, password);
|
||||
});
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Request with debug on",
|
||||
description:
|
||||
"Should complete successful request with debugging switched on",
|
||||
test: async () => {
|
||||
const config = {
|
||||
debug: true
|
||||
}
|
||||
|
||||
return await adapter.request("common/sendArr", stringData, config)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Default config",
|
||||
description:
|
||||
|
||||
@@ -12,7 +12,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
return adapter.startComputeJob("/Public/app/common/sendArr", data);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedProperties = ["id", "applicationName", "attributes"]
|
||||
const expectedProperties = ["id", "applicationName", "attributes"];
|
||||
return validate(expectedProperties, res);
|
||||
}
|
||||
},
|
||||
@@ -21,11 +21,22 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
description: "Should start a compute job and return the job",
|
||||
test: () => {
|
||||
const data: any = { table1: [{ col1: "first col value" }] };
|
||||
return adapter.startComputeJob("/Public/app/common/sendArr", data, {}, "", true);
|
||||
return adapter.startComputeJob(
|
||||
"/Public/app/common/sendArr",
|
||||
data,
|
||||
{},
|
||||
"",
|
||||
true
|
||||
);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
|
||||
return validate(expectedProperties, res.result);
|
||||
const expectedProperties = [
|
||||
"id",
|
||||
"state",
|
||||
"creationTimeStamp",
|
||||
"jobConditionCode"
|
||||
];
|
||||
return validate(expectedProperties, res.job);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -38,19 +49,19 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
`output;`,
|
||||
`end;`,
|
||||
`run;`
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
return adapter.executeScriptSASViya(
|
||||
'sasCode.sas',
|
||||
"sasCode.sas",
|
||||
fileLines,
|
||||
'SAS Studio compute context',
|
||||
"SAS Studio compute context",
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
|
||||
|
||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`;
|
||||
|
||||
return validateLog(expectedLogContent, res.log);
|
||||
}
|
||||
},
|
||||
@@ -58,21 +69,21 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
title: "Execute Script Viya - failed job",
|
||||
description: "Should execute sas file and return log",
|
||||
test: () => {
|
||||
const fileLines = [
|
||||
`%abort;`
|
||||
]
|
||||
|
||||
return adapter.executeScriptSASViya(
|
||||
'sasCode.sas',
|
||||
fileLines,
|
||||
'SAS Studio compute context',
|
||||
undefined,
|
||||
true
|
||||
).catch((err: any) => err )
|
||||
const fileLines = [`%abort;`];
|
||||
|
||||
return adapter
|
||||
.executeScriptSASViya(
|
||||
"sasCode.sas",
|
||||
fileLines,
|
||||
"SAS Studio compute context",
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
.catch((err: any) => err);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
|
||||
|
||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`;
|
||||
|
||||
return validateLog(expectedLogContent, res.log);
|
||||
}
|
||||
}
|
||||
@@ -80,16 +91,16 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
});
|
||||
|
||||
const validateLog = (text: string, log: string): boolean => {
|
||||
const isValid = JSON.stringify(log).includes(text)
|
||||
const isValid = JSON.stringify(log).includes(text);
|
||||
|
||||
return isValid
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||
const actualProperties = Object.keys(data);
|
||||
|
||||
const isValid = expectedProperties.every(
|
||||
(property) => actualProperties.includes(property)
|
||||
const isValid = expectedProperties.every((property) =>
|
||||
actualProperties.includes(property)
|
||||
);
|
||||
return isValid
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
@@ -185,7 +185,8 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
};
|
||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => !!error && !!error.error && !!error.error.message
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: "Single string value",
|
||||
|
||||
@@ -23,26 +23,23 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||
},
|
||||
{
|
||||
title: "Make error and capture log",
|
||||
description: "Should make an error and capture log, in the same time it is testing if debug override is working",
|
||||
description:
|
||||
"Should make an error and capture log, in the same time it is testing if debug override is working",
|
||||
test: async () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
adapter
|
||||
.request("common/makeErr", data, {debug: true})
|
||||
.then((res) => {
|
||||
//no action here, this request must throw error
|
||||
})
|
||||
.catch((err) => {
|
||||
let sasRequests = adapter.getSasRequests();
|
||||
let makeErrRequest: any =
|
||||
sasRequests.find((req) =>
|
||||
req.serviceLink.includes("makeErr")
|
||||
) || null;
|
||||
return adapter
|
||||
.request("common/makeErr", data, { debug: true })
|
||||
.catch(() => {
|
||||
const sasRequests = adapter.getSasRequests();
|
||||
const makeErrRequest: any =
|
||||
sasRequests.find((req) => req.serviceLink.includes("makeErr")) ||
|
||||
null;
|
||||
|
||||
if (!makeErrRequest) resolve(false)
|
||||
if (!makeErrRequest) return false;
|
||||
|
||||
resolve(!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0));
|
||||
});
|
||||
});
|
||||
return !!(
|
||||
makeErrRequest.logFile && makeErrRequest.logFile.length > 0
|
||||
);
|
||||
});
|
||||
},
|
||||
assertion: (response) => {
|
||||
return response;
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react-jsx",
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {
|
||||
Context,
|
||||
CsrfToken,
|
||||
EditContextInput,
|
||||
ContextAllAttributes
|
||||
} from './types'
|
||||
import { makeRequest, isUrl } from './utils'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { Context, EditContextInput, ContextAllAttributes } from './types'
|
||||
import { isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class ContextManager {
|
||||
private defaultComputeContexts = [
|
||||
@@ -29,8 +24,6 @@ export class ContextManager {
|
||||
'SAS Visual Forecasting launcher context'
|
||||
]
|
||||
|
||||
private csrfToken: CsrfToken | null = null
|
||||
|
||||
get getDefaultComputeContexts() {
|
||||
return this.defaultComputeContexts
|
||||
}
|
||||
@@ -38,28 +31,19 @@ export class ContextManager {
|
||||
return this.defaultLauncherContexts
|
||||
}
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
) {
|
||||
constructor(private serverUrl: string, private requestClient: RequestClient) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
public async getComputeContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute contexts. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
@@ -73,20 +57,14 @@ export class ContextManager {
|
||||
}
|
||||
|
||||
public async getLauncherContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/launcher/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting launcher contexts. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/launcher/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting launcher contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
@@ -184,18 +162,15 @@ export class ContextManager {
|
||||
requestBody.environment = { autoExecLines }
|
||||
}
|
||||
|
||||
const createContextRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody)
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
createContextRequest
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating compute context. ')
|
||||
})
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
requestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating compute context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -238,18 +213,15 @@ export class ContextManager {
|
||||
launchType
|
||||
}
|
||||
|
||||
const createContextRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody)
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<Context>(
|
||||
`${this.serverUrl}/launcher/contexts`,
|
||||
createContextRequest
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating launcher context. ')
|
||||
})
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/launcher/contexts`,
|
||||
requestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating launcher context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -268,14 +240,6 @@ export class ContextManager {
|
||||
true
|
||||
)
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
let originalContext
|
||||
|
||||
originalContext = await this.getComputeContextByName(
|
||||
@@ -291,39 +255,33 @@ export class ContextManager {
|
||||
)
|
||||
}
|
||||
|
||||
const { result: context, etag } = await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||
{
|
||||
headers
|
||||
}
|
||||
).catch((err) => {
|
||||
if (err && err.status === 404) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found on this server.`
|
||||
)
|
||||
}
|
||||
const { result: context, etag } = await this.requestClient
|
||||
.get<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err && err.status === 404) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found on this server.`
|
||||
)
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
// An If-Match header with the value of the last ETag for the context
|
||||
// is required to be able to update it
|
||||
// https://developer.sas.com/apis/rest/Compute/#update-a-context-definition
|
||||
headers['If-Match'] = etag
|
||||
|
||||
const updateContextRequest: RequestInit = {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
return await this.requestClient.put<Context>(
|
||||
`/compute/contexts/${context.id}`,
|
||||
{
|
||||
...context,
|
||||
...editedContext,
|
||||
attributes: { ...context.attributes, ...editedContext.attributes }
|
||||
})
|
||||
}
|
||||
|
||||
return await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
updateContextRequest
|
||||
},
|
||||
accessToken,
|
||||
{ 'If-Match': etag }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -331,20 +289,17 @@ export class ContextManager {
|
||||
contextName: string,
|
||||
accessToken?: string
|
||||
): Promise<Context> {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by name. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while getting compute context by name. '
|
||||
)
|
||||
})
|
||||
|
||||
if (!contexts || !(contexts.items && contexts.items.length)) {
|
||||
throw new Error(
|
||||
@@ -359,20 +314,16 @@ export class ContextManager {
|
||||
contextId: string,
|
||||
accessToken?: string
|
||||
): Promise<ContextAllAttributes> {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<ContextAllAttributes>(
|
||||
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by id. ')
|
||||
})
|
||||
const {
|
||||
result: context
|
||||
} = await this.requestClient
|
||||
.get<ContextAllAttributes>(
|
||||
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by id. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -381,20 +332,14 @@ export class ContextManager {
|
||||
executeScript: Function,
|
||||
accessToken?: string
|
||||
) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
})
|
||||
|
||||
const contextsList = contexts.items || []
|
||||
const executableContexts: any[] = []
|
||||
@@ -471,14 +416,9 @@ export class ContextManager {
|
||||
|
||||
const context = await this.getComputeContextByName(contextName, accessToken)
|
||||
|
||||
const deleteContextRequest: RequestInit = {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
}
|
||||
|
||||
return await this.request<Context>(
|
||||
return await this.requestClient.delete<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
deleteContextRequest
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
@@ -486,34 +426,6 @@ export class ContextManager {
|
||||
|
||||
// TODO: implement deleteLauncherContext method
|
||||
|
||||
private async request<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while making request in Context Manager. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private validateContextName(name: string) {
|
||||
if (!name) throw new Error('Context name is required.')
|
||||
}
|
||||
|
||||
@@ -1,115 +1,70 @@
|
||||
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
||||
import { CsrfToken } from './types/CsrfToken'
|
||||
import { isUrl } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse } from './types'
|
||||
|
||||
const requestRetryLimit = 5
|
||||
import { ErrorResponse, LoginRequiredError } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class FileUploader {
|
||||
constructor(
|
||||
private appLoc: string,
|
||||
private serverUrl: string,
|
||||
serverUrl: string,
|
||||
private jobsPath: string,
|
||||
private setCsrfTokenWeb: any,
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
private retryCount = 0
|
||||
|
||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (files?.length < 1)
|
||||
reject(new ErrorResponse('At least one file must be provided.'))
|
||||
if (!sasJob || sasJob === '')
|
||||
reject(new ErrorResponse('sasJob must be provided.'))
|
||||
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 = ''
|
||||
let paramsString = ''
|
||||
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
}
|
||||
}
|
||||
|
||||
const program = this.appLoc
|
||||
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.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)
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
return this.requestClient
|
||||
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||
.then((res) =>
|
||||
typeof res.result === 'string' ? JSON.parse(res.result) : res.result
|
||||
)
|
||||
.catch((err: Error) => {
|
||||
if (err instanceof LoginRequiredError) {
|
||||
return Promise.reject(
|
||||
new ErrorResponse('You must be logged in to upload a file.', err)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const program = this.appLoc
|
||||
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache'
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
if (this.csrfToken) formData.append('_csrf', this.csrfToken.value)
|
||||
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
referrerPolicy: 'same-origin',
|
||||
headers
|
||||
return Promise.reject(
|
||||
new ErrorResponse('File upload request failed.', err)
|
||||
)
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers.get(tokenHeader)
|
||||
this.csrfToken = {
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
}
|
||||
|
||||
this.setCsrfTokenWeb(this.csrfToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.text()
|
||||
})
|
||||
.then((responseText) => {
|
||||
if (isLogInRequired(responseText))
|
||||
reject(new ErrorResponse('You must be logged in to upload a file.'))
|
||||
|
||||
if (needsRetry(responseText)) {
|
||||
if (this.retryCount < requestRetryLimit) {
|
||||
this.retryCount++
|
||||
this.uploadFile(sasJob, files, params).then(
|
||||
(res: any) => resolve(res),
|
||||
(err: any) => reject(err)
|
||||
)
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
reject(responseText)
|
||||
}
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(responseText))
|
||||
} catch (e) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Error while parsing json from upload response.',
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(new ErrorResponse('Upload request failed.', err))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { isUrl } from './utils'
|
||||
|
||||
/**
|
||||
@@ -5,8 +6,11 @@ import { isUrl } from './utils'
|
||||
*
|
||||
*/
|
||||
export class SAS9ApiClient {
|
||||
private httpClient: AxiosInstance
|
||||
|
||||
constructor(private serverUrl: string) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.httpClient = axios.create({ baseURL: this.serverUrl })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,18 +42,18 @@ export class SAS9ApiClient {
|
||||
repositoryName: string
|
||||
) {
|
||||
const requestPayload = linesOfCode.join('\n')
|
||||
const executeScriptRequest = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: `command=${requestPayload}`
|
||||
}
|
||||
const executeScriptResponse = await fetch(
|
||||
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
executeScriptRequest
|
||||
).then((res) => res.text())
|
||||
|
||||
return executeScriptResponse
|
||||
const executeScriptResponse = await this.httpClient.put(
|
||||
`/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
`command=${requestPayload}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
responseType: 'text'
|
||||
}
|
||||
)
|
||||
|
||||
return executeScriptResponse.data
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1071
src/SASjs.ts
1071
src/SASjs.ts
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
||||
import { asyncForEach, makeRequest, isUrl } from './utils'
|
||||
import { asyncForEach, isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
const MAX_SESSION_COUNT = 1
|
||||
const RETRY_LIMIT: number = 3
|
||||
@@ -14,14 +15,13 @@ export class SessionManager {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private contextName: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
private sessions: Session[] = []
|
||||
private currentContext: Context | null = null
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private _debug: boolean = false
|
||||
private printedSessionState = {
|
||||
printed: false,
|
||||
@@ -58,20 +58,13 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
async clearSession(id: string, accessToken?: string) {
|
||||
const deleteSessionRequest = {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
return await this.request<Session>(
|
||||
`${this.serverUrl}/compute/sessions/${id}`,
|
||||
deleteSessionRequest
|
||||
)
|
||||
return await this.requestClient
|
||||
.delete<Session>(`/compute/sessions/${id}`, accessToken)
|
||||
.then(() => {
|
||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(err, 'Error while deleting session. ')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,17 +91,20 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
private async createAndWaitForSession(accessToken?: string) {
|
||||
const createSessionRequest = {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
const { result: createdSession, etag } = await this.request<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
|
||||
createSessionRequest
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const {
|
||||
result: createdSession,
|
||||
etag
|
||||
} = await this.requestClient
|
||||
.post<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${
|
||||
this.currentContext!.id
|
||||
}/sessions`,
|
||||
{},
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
await this.waitForSession(createdSession, etag, accessToken)
|
||||
|
||||
@@ -119,13 +115,13 @@ export class SessionManager {
|
||||
|
||||
private async setCurrentContext(accessToken?: string) {
|
||||
if (!this.currentContext) {
|
||||
const { result: contexts } = await this.request<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
|
||||
headers: this.getHeaders(accessToken)
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
const contextsList =
|
||||
contexts && contexts.items && contexts.items.length
|
||||
@@ -166,10 +162,7 @@ export class SessionManager {
|
||||
accessToken?: string
|
||||
) {
|
||||
let sessionState = session.state
|
||||
const headers: any = {
|
||||
...this.getHeaders(accessToken),
|
||||
'If-None-Match': etag
|
||||
}
|
||||
|
||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||
|
||||
return new Promise(async (resolve, _) => {
|
||||
@@ -185,12 +178,10 @@ export class SessionManager {
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
const { result: state } = await this.requestSessionStatus<string>(
|
||||
const state = await this.getSessionState(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
{
|
||||
headers
|
||||
},
|
||||
'text'
|
||||
etag!,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
@@ -223,73 +214,33 @@ export class SessionManager {
|
||||
})
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
private async getSessionState(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
etag: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
return await this.requestClient
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => res.result as string)
|
||||
.catch((err) => {
|
||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||
return INTERNAL_SAS_ERROR.message
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
private async requestSessionStatus<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||
return { result: INTERNAL_SAS_ERROR.message }
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
async getVariable(sessionId: string, variable: string, accessToken?: string) {
|
||||
const getSessionVariable = {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
return await this.request<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
getSessionVariable
|
||||
).catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
return await this.requestClient
|
||||
.get<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
accessToken
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
7
src/__mocks__/axios.ts
Normal file
7
src/__mocks__/axios.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AxiosStatic } from 'axios'
|
||||
|
||||
const mockAxios = jest.genMockFromModule('axios') as AxiosStatic
|
||||
|
||||
mockAxios.create = jest.fn(() => mockAxios)
|
||||
|
||||
export default mockAxios
|
||||
156
src/auth/AuthManager.ts
Normal file
156
src/auth/AuthManager.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { isAuthorizeFormRequired } from '.'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { serialize } from '../utils'
|
||||
|
||||
export class AuthManager {
|
||||
public userName = ''
|
||||
private loginUrl: string
|
||||
private logoutUrl: string
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private serverType: ServerType,
|
||||
private requestClient: RequestClient,
|
||||
private loginCallback: () => Promise<void>
|
||||
) {
|
||||
this.loginUrl = `/SASLogon/login`
|
||||
this.logoutUrl =
|
||||
this.serverType === ServerType.Sas9
|
||||
? '/SASLogon/logout?'
|
||||
: '/SASLogon/logout.do?'
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs into the SAS server with the supplied credentials.
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
*/
|
||||
public async logIn(username: string, password: string) {
|
||||
const loginParams: any = {
|
||||
_service: 'default',
|
||||
username,
|
||||
password
|
||||
}
|
||||
|
||||
this.userName = loginParams.username
|
||||
|
||||
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||
if (isLoggedIn) {
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in loginForm) {
|
||||
loginParams[key] = loginForm[key]
|
||||
}
|
||||
const loginParamsStr = serialize(loginParams)
|
||||
|
||||
const { result: loginResponse } = await this.requestClient.post<string>(
|
||||
this.loginUrl,
|
||||
loginParamsStr,
|
||||
undefined,
|
||||
'text/plain',
|
||||
{
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
)
|
||||
|
||||
let loggedIn = isLogInSuccess(loginResponse)
|
||||
|
||||
if (!loggedIn) {
|
||||
const currentSession = await this.checkSession()
|
||||
loggedIn = currentSession.isLoggedIn
|
||||
}
|
||||
|
||||
if (loggedIn) {
|
||||
this.loginCallback()
|
||||
}
|
||||
|
||||
return {
|
||||
isLoggedIn: !!loggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
*/
|
||||
public async checkSession() {
|
||||
const { result: loginResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
const responseText = loginResponse
|
||||
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
||||
let loginForm: any = null
|
||||
|
||||
if (!isLoggedIn) {
|
||||
loginForm = await this.getLoginForm(responseText)
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
isLoggedIn,
|
||||
userName: this.userName,
|
||||
loginForm
|
||||
})
|
||||
}
|
||||
|
||||
private getLoginForm(response: any) {
|
||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
|
||||
const matches = pattern.exec(response)
|
||||
const formInputs: any = {}
|
||||
|
||||
if (matches && matches.length) {
|
||||
this.setLoginUrl(matches)
|
||||
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
|
||||
|
||||
if (inputs) {
|
||||
inputs.forEach((inputStr: string) => {
|
||||
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
|
||||
|
||||
if (valueMatch && valueMatch.length) {
|
||||
formInputs[valueMatch[1]] = valueMatch[2]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(formInputs).length ? formInputs : null
|
||||
}
|
||||
|
||||
private setLoginUrl = (matches: RegExpExecArray) => {
|
||||
let parsedURL = matches[1].replace(/\?.*/, '')
|
||||
if (parsedURL[0] === '/') {
|
||||
parsedURL = parsedURL.substr(1)
|
||||
|
||||
const tempLoginLink = this.serverUrl
|
||||
? `${this.serverUrl}/${parsedURL}`
|
||||
: `${parsedURL}`
|
||||
|
||||
const loginUrl = tempLoginLink
|
||||
|
||||
this.loginUrl =
|
||||
this.serverType === ServerType.SasViya
|
||||
? tempLoginLink
|
||||
: loginUrl.replace('.do', '')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
*/
|
||||
public logOut() {
|
||||
this.requestClient.clearCsrfTokens()
|
||||
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
|
||||
}
|
||||
}
|
||||
|
||||
const isLogInSuccess = (response: string): boolean =>
|
||||
/You have signed in/gm.test(response)
|
||||
3
src/auth/index.ts
Normal file
3
src/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './AuthManager'
|
||||
export * from './isAuthorizeFormRequired'
|
||||
export * from './isLoginRequired'
|
||||
217
src/auth/spec/AuthManager.spec.ts
Normal file
217
src/auth/spec/AuthManager.spec.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { AuthManager } from '../AuthManager'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
mockLoginAuthoriseRequiredResponse,
|
||||
mockLoginSuccessResponse
|
||||
} from './mockResponses'
|
||||
import { serialize } from '../../utils'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('AuthManager', () => {
|
||||
const authCallback = jest.fn().mockImplementation(() => Promise.resolve())
|
||||
const serverUrl = 'http://test-server.com'
|
||||
const serverType = ServerType.SasViya
|
||||
const userName = 'test-username'
|
||||
const password = 'test-password'
|
||||
const requestClient = new RequestClient(serverUrl)
|
||||
|
||||
beforeAll(() => {
|
||||
dotenv.config()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should instantiate and set the correct URLs for a Viya server', () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
|
||||
expect(authManager).toBeTruthy()
|
||||
expect((authManager as any).serverUrl).toEqual(serverUrl)
|
||||
expect((authManager as any).serverType).toEqual(serverType)
|
||||
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
|
||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout.do?')
|
||||
})
|
||||
|
||||
it('should instantiate and set the correct URLs for a SAS9 server', () => {
|
||||
const authCallback = () => Promise.resolve()
|
||||
const serverType = ServerType.Sas9
|
||||
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
|
||||
expect(authManager).toBeTruthy()
|
||||
expect((authManager as any).serverUrl).toEqual(serverUrl)
|
||||
expect((authManager as any).serverType).toEqual(serverType)
|
||||
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
|
||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
|
||||
})
|
||||
|
||||
it('should call the auth callback and return when already logged in', async (done) => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName: 'test',
|
||||
loginForm: 'test'
|
||||
})
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should post a login request to the server if not logged in', async (done) => {
|
||||
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)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should parse and submit the authorisation form when necessary', async (done) => {
|
||||
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
|
||||
)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async (done) => {
|
||||
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, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async (done) => {
|
||||
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, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
2
src/auth/spec/mockResponses.ts
Normal file
2
src/auth/spec/mockResponses.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
||||
export const mockLoginSuccessResponse = `You have signed in`
|
||||
24
src/file/generateFileUploadForm.ts
Normal file
24
src/file/generateFileUploadForm.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { convertToCSV } from '../utils/convertToCsv'
|
||||
|
||||
export const generateFileUploadForm = (
|
||||
formData: FormData,
|
||||
data: any
|
||||
): FormData => {
|
||||
for (const tableName in data) {
|
||||
const name = tableName
|
||||
const csv = convertToCSV(data[tableName])
|
||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||
throw new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
}
|
||||
|
||||
const file = new Blob([csv], {
|
||||
type: 'application/csv'
|
||||
})
|
||||
|
||||
formData.append(name, file, `${name}.csv`)
|
||||
}
|
||||
|
||||
return formData
|
||||
}
|
||||
31
src/file/generateTableUploadForm.ts
Normal file
31
src/file/generateTableUploadForm.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { convertToCSV } from '../utils/convertToCsv'
|
||||
import { splitChunks } from '../utils/splitChunks'
|
||||
|
||||
export const generateTableUploadForm = (formData: FormData, data: any) => {
|
||||
const sasjsTables = []
|
||||
const requestParams: any = {}
|
||||
let tableCounter = 0
|
||||
for (const tableName in data) {
|
||||
tableCounter++
|
||||
sasjsTables.push(tableName)
|
||||
const csv = convertToCSV(data[tableName])
|
||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||
throw new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
}
|
||||
// if csv has length more then 16k, send in chunks
|
||||
if (csv.length > 16000) {
|
||||
const csvChunks = splitChunks(csv)
|
||||
// append chunks to form data with same key
|
||||
csvChunks.map((chunk) => {
|
||||
formData.append(`sasjs${tableCounter}data`, chunk)
|
||||
})
|
||||
} else {
|
||||
requestParams[`sasjs${tableCounter}data`] = csv
|
||||
}
|
||||
}
|
||||
requestParams['sasjs_tables'] = sasjsTables.join(' ')
|
||||
|
||||
return { formData, requestParams }
|
||||
}
|
||||
71
src/job-execution/ComputeJobExecutor.ts
Normal file
71
src/job-execution/ComputeJobExecutor.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse } from '..'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { ComputeJobExecutionError, LoginRequiredError } from '../types'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
|
||||
super(serverUrl, ServerType.SasViya)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
const waitForResult = true
|
||||
const expectWebout = true
|
||||
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.sasViyaApiClient
|
||||
?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
waitForResult,
|
||||
expectWebout
|
||||
)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
resolve(response.result)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof ComputeJobExecutionError) {
|
||||
this.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
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
}
|
||||
68
src/job-execution/JesJobExecutor.ts
Normal file
68
src/job-execution/JesJobExecutor.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse } from '..'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { JobExecutionError, LoginRequiredError } from '../types'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export class JesJobExecutor extends BaseJobExecutor {
|
||||
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
|
||||
super(serverUrl, ServerType.SasViya)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.sasViyaApiClient
|
||||
?.executeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken
|
||||
)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
resolve(response.result)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.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
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
}
|
||||
96
src/job-execution/JobExecutor.ts
Normal file
96
src/job-execution/JobExecutor.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { SASjsRequest } from '../types'
|
||||
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
|
||||
|
||||
export type ExecuteFunction = () => Promise<any>
|
||||
|
||||
export interface JobExecutor {
|
||||
execute: (
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
) => Promise<any>
|
||||
resendWaitingRequests: () => Promise<void>
|
||||
getRequests: () => SASjsRequest[]
|
||||
clearRequests: () => void
|
||||
}
|
||||
|
||||
export abstract class BaseJobExecutor implements JobExecutor {
|
||||
constructor(protected serverUrl: string, protected serverType: ServerType) {}
|
||||
|
||||
private waitingRequests: ExecuteFunction[] = []
|
||||
private requests: SASjsRequest[] = []
|
||||
|
||||
abstract execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string | undefined
|
||||
): Promise<any>
|
||||
|
||||
resendWaitingRequests = async () => {
|
||||
await asyncForEach(
|
||||
this.waitingRequests,
|
||||
async (waitingRequest: ExecuteFunction) => {
|
||||
await waitingRequest()
|
||||
}
|
||||
)
|
||||
|
||||
this.waitingRequests = []
|
||||
return
|
||||
}
|
||||
|
||||
getRequests = () => this.requests
|
||||
|
||||
clearRequests = () => {
|
||||
this.requests = []
|
||||
}
|
||||
|
||||
protected appendWaitingRequest(request: ExecuteFunction) {
|
||||
this.waitingRequests.push(request)
|
||||
}
|
||||
|
||||
protected appendRequest(response: any, program: string, debug: boolean) {
|
||||
let sourceCode = ''
|
||||
let generatedCode = ''
|
||||
let sasWork = null
|
||||
|
||||
if (debug) {
|
||||
if (response?.result && response?.log) {
|
||||
sourceCode = parseSourceCode(response.log)
|
||||
generatedCode = parseGeneratedCode(response.log)
|
||||
|
||||
if (response.log) {
|
||||
sasWork = response.log
|
||||
} else {
|
||||
sasWork = response.result.WORK
|
||||
}
|
||||
} else if (response?.result) {
|
||||
sourceCode = parseSourceCode(response.result)
|
||||
generatedCode = parseGeneratedCode(response.result)
|
||||
sasWork = response.result.WORK
|
||||
}
|
||||
}
|
||||
|
||||
const stringifiedResult =
|
||||
typeof response?.result === 'string'
|
||||
? response?.result
|
||||
: JSON.stringify(response?.result, null, 2)
|
||||
|
||||
this.requests.push({
|
||||
logFile: response?.log || stringifiedResult || response,
|
||||
serviceLink: program,
|
||||
timestamp: new Date(),
|
||||
sourceCode,
|
||||
generatedCode,
|
||||
SASWORK: sasWork
|
||||
})
|
||||
|
||||
if (this.requests.length > 20) {
|
||||
this.requests.splice(0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
215
src/job-execution/WebJobExecutor.ts
Normal file
215
src/job-execution/WebJobExecutor.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse, JobExecutionError, LoginRequiredError } from '..'
|
||||
import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
||||
import { generateTableUploadForm } from '../file/generateTableUploadForm'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { isRelativePath } from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export interface WaitingRequstPromise {
|
||||
promise: Promise<any> | null
|
||||
resolve: any
|
||||
reject: any
|
||||
}
|
||||
export class WebJobExecutor extends BaseJobExecutor {
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
serverType: ServerType,
|
||||
private jobsPath: string,
|
||||
private requestClient: RequestClient,
|
||||
private sasViyaApiClient: SASViyaApiClient
|
||||
) {
|
||||
super(serverUrl, serverType)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
const program = isRelativePath(sasJob)
|
||||
? config.appLoc
|
||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
: sasJob
|
||||
const jobUri =
|
||||
config.serverType === ServerType.SasViya
|
||||
? await this.getJobUri(sasJob)
|
||||
: ''
|
||||
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
||||
jobUri.length > 0
|
||||
? '__program=' + program + '&_job=' + jobUri
|
||||
: '_program=' + program
|
||||
}`
|
||||
|
||||
let requestParams = {
|
||||
...this.getRequestParams(config)
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
|
||||
if (data) {
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
if (
|
||||
config.serverType === ServerType.Sas9 ||
|
||||
stringifiedData.length > 500000 ||
|
||||
stringifiedData.includes(';')
|
||||
) {
|
||||
// file upload approach
|
||||
try {
|
||||
formData = generateFileUploadForm(formData, data)
|
||||
} catch (e) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
} else {
|
||||
// param based approach
|
||||
try {
|
||||
const {
|
||||
formData: newFormData,
|
||||
requestParams: params
|
||||
} = generateTableUploadForm(formData, data)
|
||||
formData = newFormData
|
||||
requestParams = { ...requestParams, ...params }
|
||||
} catch (e) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in requestParams) {
|
||||
if (requestParams.hasOwnProperty(key)) {
|
||||
formData.append(key, requestParams[key])
|
||||
}
|
||||
}
|
||||
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.requestClient!.post(apiUrl, formData, undefined)
|
||||
.then(async (res) => {
|
||||
if (this.serverType === ServerType.SasViya && config.debug) {
|
||||
const jsonResponse = await this.parseSasViyaDebugResponse(
|
||||
res.result as string
|
||||
)
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
resolve(jsonResponse)
|
||||
}
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
resolve(res.result)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.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
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
|
||||
private parseSasViyaDebugResponse = async (response: string) => {
|
||||
const iframeStart = response.split(
|
||||
'<iframe style="width: 99%; height: 500px" src="'
|
||||
)[1]
|
||||
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null
|
||||
if (!jsonUrl) {
|
||||
throw new Error('Unable to find webout file URL.')
|
||||
}
|
||||
|
||||
return this.requestClient
|
||||
.get(this.serverUrl + jsonUrl, undefined)
|
||||
.then((res) => res.result)
|
||||
}
|
||||
|
||||
private async getJobUri(sasJob: string) {
|
||||
if (!this.sasViyaApiClient) return ''
|
||||
let uri = ''
|
||||
|
||||
let folderPath
|
||||
let jobName: string
|
||||
if (isRelativePath(sasJob)) {
|
||||
const folderPathParts = sasJob.split('/')
|
||||
folderPath = folderPathParts.length > 1 ? folderPathParts[0] : ''
|
||||
jobName = folderPathParts.length > 1 ? folderPathParts[1] : ''
|
||||
} else {
|
||||
const folderPathParts = sasJob.split('/')
|
||||
jobName = folderPathParts.pop() || ''
|
||||
folderPath = folderPathParts.join('/')
|
||||
}
|
||||
|
||||
if (!jobName) {
|
||||
throw new Error('Job name is empty, null or undefined.')
|
||||
}
|
||||
|
||||
const locJobs = await this.sasViyaApiClient.getJobsInFolder(folderPath)
|
||||
if (locJobs) {
|
||||
const job = locJobs.find(
|
||||
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
|
||||
)
|
||||
if (job) {
|
||||
uri = job.uri
|
||||
}
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
private getRequestParams(config: any): any {
|
||||
const requestParams: any = {}
|
||||
|
||||
if (config.debug) {
|
||||
requestParams['_omittextlog'] = 'false'
|
||||
requestParams['_omitsessionresults'] = 'false'
|
||||
|
||||
requestParams['_debug'] = 131
|
||||
}
|
||||
|
||||
return requestParams
|
||||
}
|
||||
|
||||
private parseSAS9ErrorResponse(response: string) {
|
||||
const logLines = response.split('\n')
|
||||
const parsedLines: string[] = []
|
||||
let firstErrorLineIndex: number = -1
|
||||
|
||||
logLines.map((line: string, index: number) => {
|
||||
if (
|
||||
line.toLowerCase().includes('error') &&
|
||||
!line.toLowerCase().includes('this request completed with errors.') &&
|
||||
firstErrorLineIndex === -1
|
||||
) {
|
||||
firstErrorLineIndex = index
|
||||
}
|
||||
})
|
||||
|
||||
for (let i = firstErrorLineIndex - 10; i <= firstErrorLineIndex + 10; i++) {
|
||||
parsedLines.push(logLines[i])
|
||||
}
|
||||
|
||||
return parsedLines.join(', ')
|
||||
}
|
||||
}
|
||||
4
src/job-execution/index.ts
Normal file
4
src/job-execution/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './ComputeJobExecutor'
|
||||
export * from './JesJobExecutor'
|
||||
export * from './JobExecutor'
|
||||
export * from './WebJobExecutor'
|
||||
507
src/request/RequestClient.ts
Normal file
507
src/request/RequestClient.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { CsrfToken, JobExecutionError } from '..'
|
||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||
import { LoginRequiredError } from '../types'
|
||||
import { AuthorizeError } from '../types/AuthorizeError'
|
||||
import { NotFoundError } from '../types/NotFoundError'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
put<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
delete<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
|
||||
clearCsrfTokens(): void
|
||||
}
|
||||
|
||||
export class RequestClient implements HttpClient {
|
||||
private csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||
private fileUploadCsrfToken: CsrfToken | undefined
|
||||
private httpClient: AxiosInstance
|
||||
|
||||
constructor(private 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
||||
return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
|
||||
}
|
||||
|
||||
public clearCsrfTokens() {
|
||||
this.csrfToken = { headerName: '', value: '' }
|
||||
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
||||
}
|
||||
|
||||
public async get<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
headers,
|
||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||
withCredentials: true
|
||||
}
|
||||
if (contentType === 'text/plain') {
|
||||
requestConfig.transformResponse = undefined
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.get<T>(url, requestConfig)
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
|
||||
(err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while executing handle error callback. '
|
||||
)
|
||||
}
|
||||
)
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while handling error. ')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
contentType = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.post<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async put<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, 'application/json'),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.put<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.put<T>(url, data, accessToken, overrideHeaders)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async delete<T>(
|
||||
url: string,
|
||||
accessToken?: string
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
return this.httpClient
|
||||
.delete<T>(url, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () => this.delete<T>(url, accessToken))
|
||||
})
|
||||
}
|
||||
|
||||
public async patch<T>(
|
||||
url: string,
|
||||
data: any = {},
|
||||
accessToken?: string
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
return this.httpClient
|
||||
.patch<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.patch<T>(url, data, accessToken)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async uploadFile(
|
||||
url: string,
|
||||
content: string,
|
||||
accessToken?: string
|
||||
): Promise<any> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
if (this.fileUploadCsrfToken?.value) {
|
||||
headers[
|
||||
this.fileUploadCsrfToken.headerName
|
||||
] = this.fileUploadCsrfToken.value
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.post(url, content, { headers })
|
||||
return {
|
||||
result: response.data,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response = e.response as AxiosResponse
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetFileUploadCsrfToken(response)
|
||||
|
||||
if (this.fileUploadCsrfToken) {
|
||||
return this.uploadFile(url, content, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public authorize = async (response: string) => {
|
||||
let authUrl: string | null = null
|
||||
const params: any = {}
|
||||
|
||||
const responseBody = response.split('<body>')[1].split('</body>')[0]
|
||||
const bodyElement = document.createElement('div')
|
||||
bodyElement.innerHTML = responseBody
|
||||
|
||||
const form = bodyElement.querySelector('#application_authorization')
|
||||
authUrl = form ? this.baseUrl + form.getAttribute('action') : null
|
||||
|
||||
const inputs: any = form?.querySelectorAll('input')
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.name === 'user_oauth_approval') {
|
||||
input.value = 'true'
|
||||
}
|
||||
|
||||
params[input.name] = input.value
|
||||
}
|
||||
|
||||
const csrfTokenKey = Object.keys(params).find((k) =>
|
||||
k?.toLowerCase().includes('csrf')
|
||||
)
|
||||
if (csrfTokenKey) {
|
||||
this.csrfToken.value = params[csrfTokenKey]
|
||||
this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token'
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
formData.append(key, params[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (!authUrl) {
|
||||
throw new Error('Auth Form URL is null or undefined.')
|
||||
}
|
||||
|
||||
return await this.httpClient
|
||||
.post(authUrl, formData, {
|
||||
responseType: 'text',
|
||||
headers: { Accept: '*/*', 'Content-Type': 'text/plain' }
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
private getHeaders = (
|
||||
accessToken: string | undefined,
|
||||
contentType: string
|
||||
) => {
|
||||
const headers: any = {}
|
||||
|
||||
if (contentType !== 'application/x-www-form-urlencoded') {
|
||||
headers['Content-Type'] = contentType
|
||||
}
|
||||
|
||||
if (contentType === 'application/json') {
|
||||
headers.Accept = 'application/json'
|
||||
} else {
|
||||
headers.Accept = '*/*'
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
if (token) {
|
||||
this.fileUploadCsrfToken = token
|
||||
}
|
||||
}
|
||||
|
||||
private parseAndSetCsrfToken = (response: AxiosResponse) => {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
if (token) {
|
||||
this.csrfToken = token
|
||||
}
|
||||
}
|
||||
|
||||
private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => {
|
||||
const tokenHeader = (response.headers[
|
||||
'x-csrf-header'
|
||||
] as string)?.toLowerCase()
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers[tokenHeader]
|
||||
const csrfToken = {
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
}
|
||||
|
||||
return csrfToken
|
||||
}
|
||||
}
|
||||
|
||||
private handleError = async (e: any, callback: any) => {
|
||||
const response = e.response as AxiosResponse
|
||||
|
||||
if (e instanceof AuthorizeError) {
|
||||
const res = await this.httpClient
|
||||
.get(e.confirmUrl, {
|
||||
responseType: 'text',
|
||||
headers: { 'Content-Type': 'text/plain', Accept: '*/*' }
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting error confirmUrl. ')
|
||||
})
|
||||
|
||||
if (isAuthorizeFormRequired(res?.data as string)) {
|
||||
await this.authorize(res.data as string).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while authorizing request. ')
|
||||
})
|
||||
}
|
||||
|
||||
return await callback().catch((err: any) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while executing callback in handleError. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
this.clearCsrfTokens()
|
||||
}
|
||||
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
|
||||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
||||
return await callback().catch((err: any) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while executing callback in handleError. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
throw e
|
||||
} else if (response?.status === 404) {
|
||||
throw new NotFoundError(response.config.url!)
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
|
||||
private parseResponse<T>(response: AxiosResponse<any>) {
|
||||
const etag = response?.headers ? response.headers['etag'] : ''
|
||||
let parsedResponse
|
||||
let includeSAS9Log: boolean = false
|
||||
|
||||
try {
|
||||
if (typeof response.data === 'string') {
|
||||
parsedResponse = JSON.parse(response.data)
|
||||
} else {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
parsedResponse = JSON.parse(parseWeboutResponse(response.data))
|
||||
} catch {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
|
||||
includeSAS9Log = true
|
||||
}
|
||||
|
||||
let responseToReturn: { result: T; etag: any; log?: string } = {
|
||||
result: parsedResponse as T,
|
||||
etag
|
||||
}
|
||||
|
||||
if (includeSAS9Log) {
|
||||
responseToReturn.log = response.data
|
||||
}
|
||||
|
||||
return responseToReturn
|
||||
}
|
||||
}
|
||||
|
||||
const throwIfError = (response: AxiosResponse) => {
|
||||
if (response.status === 401) {
|
||||
throw new LoginRequiredError()
|
||||
}
|
||||
|
||||
if (response.data?.entityID?.includes('login')) {
|
||||
throw new LoginRequiredError()
|
||||
}
|
||||
|
||||
if (
|
||||
typeof response.data === 'string' &&
|
||||
isAuthorizeFormRequired(response.data)
|
||||
) {
|
||||
throw new AuthorizeError(
|
||||
'Authorization required',
|
||||
response.request.responseURL
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
typeof response.data === 'string' &&
|
||||
isLogInRequired(response.data) &&
|
||||
!response.config?.url?.includes('/SASLogon/login')
|
||||
) {
|
||||
throw new LoginRequiredError()
|
||||
}
|
||||
if (response.data?.auth_request) {
|
||||
const authorizeRequestUrl = response.request.responseURL
|
||||
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
|
||||
}
|
||||
|
||||
const error = parseError(response.data as string)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const parseError = (data: string) => {
|
||||
try {
|
||||
const responseJson = JSON.parse(data?.replace(/[\n\r]/g, ' '))
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,16 @@
|
||||
import { ContextManager } from '../ContextManager'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import * as dotenv from 'dotenv'
|
||||
import axios from 'axios'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('ContextManager', () => {
|
||||
let originalFetch: any
|
||||
let fetchCallNumber = 0
|
||||
|
||||
const fakeGlobalFetch = (fakeResponses: object[]) => {
|
||||
;(global as any).fetch = jest.fn().mockImplementation(() => {
|
||||
const fakeResponse = fakeResponses[fetchCallNumber]
|
||||
|
||||
if (
|
||||
fetchCallNumber !== fakeResponses.length &&
|
||||
fakeResponses.length > 1
|
||||
) {
|
||||
if (fetchCallNumber + 1 === fakeResponses.length) fetchCallNumber = 0
|
||||
else fetchCallNumber += 1
|
||||
} else {
|
||||
fetchCallNumber = 0
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => '' },
|
||||
json: () => Promise.resolve(fakeResponse)
|
||||
})
|
||||
})
|
||||
}
|
||||
dotenv.config()
|
||||
|
||||
const contextManager = new ContextManager(
|
||||
process.env.SERVER_URL as string,
|
||||
() => {}
|
||||
new RequestClient(process.env.SERVER_URL as string)
|
||||
)
|
||||
|
||||
const defaultComputeContexts = contextManager.getDefaultComputeContexts
|
||||
@@ -43,14 +25,6 @@ describe('ContextManager', () => {
|
||||
Math.floor(Math.random() * defaultLauncherContexts.length)
|
||||
]
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = (global as any).fetch
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
;(global as any).fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('getComputeContexts', () => {
|
||||
it('should fetch compute contexts', async () => {
|
||||
const sampleComputeContext = {
|
||||
@@ -65,7 +39,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(contextManager.getComputeContexts()).resolves.toEqual([
|
||||
sampleComputeContext
|
||||
@@ -87,7 +63,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(contextManager.getLauncherContexts()).resolves.toEqual([
|
||||
sampleComputeContext
|
||||
@@ -137,7 +115,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -176,10 +156,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingComputeContexts,
|
||||
sampleResponseCreatedComputeContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -226,10 +209,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingComputeContexts,
|
||||
sampleResponseCreatedComputeContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -287,11 +273,16 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingComputeContexts,
|
||||
sampleResponseCreatedLauncherContext,
|
||||
sampleResponseCreatedComputeContext
|
||||
])
|
||||
mockedAxios.get
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedLauncherContext })
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -346,7 +337,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleLauncherContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createLauncherContext(contextName, 'Test Description')
|
||||
@@ -380,10 +373,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewLauncherContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingLauncherContext,
|
||||
sampleResponseCreatedLauncherContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingLauncherContext })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedLauncherContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createLauncherContext(contextName, 'Test Description')
|
||||
@@ -448,7 +444,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponseGetComputeContextByName])
|
||||
mockedAxios.put.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseGetComputeContextByName })
|
||||
)
|
||||
|
||||
const expectedResponse = {
|
||||
etag: '',
|
||||
@@ -475,7 +473,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const user = 'testUser'
|
||||
|
||||
@@ -508,7 +508,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const fakedExecuteScript = async () => {
|
||||
return Promise.resolve({ log: '' })
|
||||
@@ -567,10 +569,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseGetComputeContextByName,
|
||||
sampleResponseDeletedContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseGetComputeContextByName })
|
||||
)
|
||||
|
||||
mockedAxios.delete.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseDeletedContext })
|
||||
)
|
||||
|
||||
const expectedResponse = {
|
||||
etag: '',
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { FileUploader } from '../FileUploader'
|
||||
import { UploadFile } from '../types'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import axios from 'axios'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
const sampleResponse = `{
|
||||
"SYSUSERID": "cas",
|
||||
@@ -24,39 +28,22 @@ const prepareFilesAndParams = () => {
|
||||
}
|
||||
|
||||
describe('FileUploader', () => {
|
||||
let originalFetch: any
|
||||
const fileUploader = new FileUploader(
|
||||
'/sample/apploc',
|
||||
'https://sample.server.com',
|
||||
'/jobs/path',
|
||||
null,
|
||||
null
|
||||
new RequestClient('https://sample.server.com')
|
||||
)
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = (global as any).fetch
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
;(global as any).fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.resolve(sampleResponse)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
;(global as any).fetch = originalFetch
|
||||
})
|
||||
|
||||
it('should upload successfully', async (done) => {
|
||||
const sasJob = 'test/upload'
|
||||
const { files, params } = prepareFilesAndParams()
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
|
||||
expect(JSON.stringify(res)).toEqual(
|
||||
JSON.stringify(JSON.parse(sampleResponse))
|
||||
)
|
||||
expect(res).toEqual(JSON.parse(sampleResponse))
|
||||
done()
|
||||
})
|
||||
})
|
||||
@@ -83,10 +70,8 @@ describe('FileUploader', () => {
|
||||
})
|
||||
|
||||
it('should throw an error when login is required', async (done) => {
|
||||
;(global as any).fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.resolve('<form action="Logon">')
|
||||
})
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<form action="Logon">' })
|
||||
)
|
||||
|
||||
const sasJob = 'test'
|
||||
@@ -101,35 +86,29 @@ describe('FileUploader', () => {
|
||||
})
|
||||
|
||||
it('should throw an error when invalid JSON is returned by the server', async (done) => {
|
||||
;(global as any).fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.resolve('{invalid: "json"')
|
||||
})
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: '{invalid: "json"' })
|
||||
)
|
||||
|
||||
const sasJob = 'test'
|
||||
const { files, params } = prepareFilesAndParams()
|
||||
|
||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
||||
expect(err.error.message).toEqual(
|
||||
'Error while parsing json from upload response.'
|
||||
)
|
||||
expect(err.error.message).toEqual('File upload request failed.')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error when the server request fails', async (done) => {
|
||||
;(global as any).fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.reject('{message: "Server error"}')
|
||||
})
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.reject({ data: '{message: "Server error"}' })
|
||||
)
|
||||
|
||||
const sasJob = 'test'
|
||||
const { files, params } = prepareFilesAndParams()
|
||||
|
||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
||||
expect(err.error.message).toEqual('Upload request failed.')
|
||||
expect(err.error.message).toEqual('File upload request failed.')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user