mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 01:14:36 +00:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
85a530ae5a | ||
|
|
2ea49a425f | ||
|
|
3c894c4147 | ||
|
|
23d151c919 | ||
|
|
6d1c4ff81a | ||
|
|
0eba6bdcf4 | ||
|
|
d7ecaf5932 | ||
|
|
1fc6db114d | ||
|
|
8d203b8df4 | ||
|
|
39924ff078 | ||
|
|
de25f106ec | ||
|
|
e0d85f458b | ||
|
|
3a9cd46e6e | ||
|
|
301edab8ad | ||
|
|
aed39c2ec4 | ||
|
|
e31774ae9d | ||
|
|
00f09179a8 | ||
|
|
4196901e01 | ||
|
|
bf35dd072a | ||
|
|
75e3fd018d | ||
|
|
965dfff7c6 | ||
|
|
ff64dd22ad | ||
|
|
e7cceab065 | ||
|
|
f789b8f7a2 | ||
|
|
c0b82c5125 | ||
|
|
1c1b5baefe | ||
|
|
8b17aeaea2 | ||
|
|
cb0d03c965 | ||
|
|
9e77f3d64e | ||
|
|
25f61815dc | ||
|
|
3a2252e69c | ||
|
|
8a08980e6a | ||
|
|
d0f31771ad | ||
|
|
e9e2c9372d | ||
|
|
70c4a095a0 | ||
|
|
82e2fc4445 | ||
|
|
6661d81fdf | ||
|
|
e76abaafa8 | ||
|
|
fbfc1c05d6 | ||
|
|
839c211c64 | ||
|
|
f3ff82143a | ||
|
|
0dd0abae87 | ||
|
|
13781c993e | ||
|
|
7616cacbec | ||
|
|
cab7d3c012 | ||
|
|
dfce676fdf | ||
|
|
1890cab623 | ||
|
|
4307d8fe43 | ||
|
|
8df6fdbee6 | ||
|
|
ac5c2a3088 | ||
|
|
0212b677ae | ||
|
|
1a0d62d8f3 | ||
|
|
8f4d1c7aea | ||
|
|
2a4735c6f2 | ||
|
|
5a2ee88cbc | ||
|
|
b23f199334 | ||
|
|
ed5dabee9f | ||
|
|
0c88c5a522 | ||
|
|
640e7015c8 | ||
|
|
a2906abf71 | ||
|
|
2fd306f435 | ||
|
|
e3f779dbd1 | ||
|
|
1064f11663 | ||
|
|
46abc54cb0 | ||
|
|
2c808a937a | ||
|
|
52cf9a420f | ||
|
|
2d29be45f5 | ||
|
|
a44222c3ba | ||
|
|
efc82101c1 | ||
|
|
09ce2fb6be | ||
|
|
a383388e54 | ||
|
|
362078b12c | ||
|
|
9d0c3410a5 | ||
|
|
dfb9c28f3a | ||
|
|
8d155283dd | ||
|
|
d991ead86a | ||
|
|
33a202fa1c | ||
|
|
ff5463a84c | ||
|
|
aa7c3ae4a9 | ||
|
|
2e66bfde4b | ||
|
|
16e21adb20 | ||
|
|
01c5682c3d | ||
|
|
cfc8ff2837 | ||
|
|
edf25b471a | ||
|
|
bb894e6107 | ||
|
|
6b3a0cdb13 | ||
|
|
8c98a26160 | ||
|
|
bcd9310f26 | ||
|
|
57e9b67207 | ||
|
|
7bf53858f0 | ||
|
|
02780d0bcd | ||
|
|
6356aed06b | ||
|
|
69fd7b2cb5 | ||
|
|
5d1eed1494 | ||
|
|
e2e2824f37 | ||
|
|
d461135980 | ||
|
|
65fbae7610 | ||
|
|
761428502a | ||
|
|
6eb2ceaf53 | ||
|
|
66813b9824 | ||
|
|
140d8e4eac | ||
|
|
0d730e0576 | ||
|
|
ca18fcecf0 | ||
|
|
009069169f | ||
|
|
6d166efd11 | ||
|
|
1b117a67aa | ||
|
|
9037160362 | ||
|
|
505d85c256 | ||
|
|
71a3fe04a0 | ||
|
|
1555afe771 | ||
|
|
79bb27524c | ||
|
|
9651b7adb4 | ||
|
|
59e5bec731 | ||
|
|
182e66216f | ||
|
|
a2832f1e1a | ||
|
|
56f34508fa | ||
|
|
2408fd091e | ||
|
|
0e38a24664 | ||
|
|
aa643d1782 | ||
|
|
cdc91e9cda | ||
|
|
3f0590e0fe | ||
|
|
5efb294ff2 | ||
|
|
011e2d83dc | ||
|
|
e36b511530 | ||
|
|
b6a2a85d1d | ||
|
|
f1cceeb5e6 | ||
|
|
6fee2548fd | ||
|
|
91005066cf | ||
|
|
e1f17ef47d | ||
|
|
8a40071c35 | ||
|
|
430957eb3d | ||
|
|
25874be679 | ||
|
|
ed8440434f | ||
|
|
0f9884c1b6 | ||
|
|
d126a05347 | ||
|
|
3e26bbbbba | ||
|
|
982cc8f7a0 | ||
|
|
d1770698e0 | ||
|
|
b78e8617c4 | ||
|
|
3ce9ca0986 | ||
|
|
04d17c3680 | ||
|
|
d26e15f91c | ||
|
|
83c46091b3 | ||
|
|
d640d7c040 | ||
|
|
c934eb2b08 | ||
|
|
24dd5e32ad | ||
|
|
a23103b2c3 | ||
|
|
35aa4235e4 | ||
|
|
e9be1cf99a | ||
|
|
c7b0821081 | ||
|
|
4a4618dd32 | ||
|
|
d223e83c60 | ||
|
|
d1f1a20126 | ||
|
|
4b89e3762f | ||
|
|
bc110288de | ||
|
|
e94e16b52c | ||
|
|
76aacee016 | ||
|
|
1a3bd5d1f5 | ||
|
|
3f6e89d716 | ||
|
|
361ec84638 | ||
|
|
35cc1e4f62 | ||
|
|
64a976e888 | ||
|
|
7e2cb8491f | ||
|
|
2cdab7522d | ||
|
|
a07eabc408 | ||
|
|
7279c23fe2 | ||
|
|
80707d77d9 | ||
|
|
d5920c5885 | ||
|
|
6a3a6b4485 | ||
|
|
3f796b300d |
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
SERVER_URL=https://server.com
|
||||
DEFAULT_COMPUTE_CONTEXT=SAS Job Execution compute context
|
||||
9
.github/reviewer-lottery.yml
vendored
Normal file
9
.github/reviewer-lottery.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
groups:
|
||||
- name: SASjs Devs # name of the group
|
||||
reviewers: 1 # how many reviewers do you want to assign?
|
||||
usernames: # github usernames of the reviewers
|
||||
- krishna-acondy
|
||||
- YuryShkoda
|
||||
- saadjutt01
|
||||
- medjedovicm
|
||||
- allanbowe
|
||||
13
.github/workflows/assign-reviewer.yml
vendored
Normal file
13
.github/workflows/assign-reviewer.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: 'Assign Reviewer'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: uesteibar/reviewer-lottery@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GH_TOKEN }}
|
||||
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
@@ -1,2 +1,6 @@
|
||||
node_modules
|
||||
build
|
||||
|
||||
.env
|
||||
|
||||
/coverage
|
||||
@@ -14,4 +14,5 @@ What code changes have been made to achieve the intent.
|
||||
|
||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||
- [ ] All unit tests are passing (`npm test`).
|
||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||
|
||||
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
231
docs/classes/reflection-790.reflection-214.fileuploader.html
Normal file
231
docs/classes/reflection-790.reflection-214.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-790.reflection-214.sas9apiclient.html
Normal file
312
docs/classes/reflection-790.reflection-214.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1641
docs/classes/reflection-790.reflection-214.sasjs.html
Normal file
1641
docs/classes/reflection-790.reflection-214.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1444
docs/classes/reflection-790.reflection-214.sasviyaapiclient.html
Normal file
1444
docs/classes/reflection-790.reflection-214.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-790.reflection-214.sessionmanager.html
Normal file
323
docs/classes/reflection-790.reflection-214.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-804.reflection-219.fileuploader.html
Normal file
231
docs/classes/reflection-804.reflection-219.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-804.reflection-219.sas9apiclient.html
Normal file
312
docs/classes/reflection-804.reflection-219.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1670
docs/classes/reflection-804.reflection-219.sasjs.html
Normal file
1670
docs/classes/reflection-804.reflection-219.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1471
docs/classes/reflection-804.reflection-219.sasviyaapiclient.html
Normal file
1471
docs/classes/reflection-804.reflection-219.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-804.reflection-219.sessionmanager.html
Normal file
323
docs/classes/reflection-804.reflection-219.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-831.reflection-220.fileuploader.html
Normal file
231
docs/classes/reflection-831.reflection-220.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-831.reflection-220.sas9apiclient.html
Normal file
312
docs/classes/reflection-831.reflection-220.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1725
docs/classes/reflection-831.reflection-220.sasjs.html
Normal file
1725
docs/classes/reflection-831.reflection-220.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1567
docs/classes/reflection-831.reflection-220.sasviyaapiclient.html
Normal file
1567
docs/classes/reflection-831.reflection-220.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-831.reflection-220.sessionmanager.html
Normal file
323
docs/classes/reflection-831.reflection-220.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-837.reflection-220.fileuploader.html
Normal file
231
docs/classes/reflection-837.reflection-220.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-837.reflection-220.sas9apiclient.html
Normal file
312
docs/classes/reflection-837.reflection-220.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1782
docs/classes/reflection-837.reflection-220.sasjs.html
Normal file
1782
docs/classes/reflection-837.reflection-220.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1567
docs/classes/reflection-837.reflection-220.sasviyaapiclient.html
Normal file
1567
docs/classes/reflection-837.reflection-220.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-837.reflection-220.sessionmanager.html
Normal file
323
docs/classes/reflection-837.reflection-220.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
622
docs/classes/root.contextmanager.html
Normal file
622
docs/classes/root.contextmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/root.fileuploader.html
Normal file
231
docs/classes/root.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/root.sas9apiclient.html
Normal file
312
docs/classes/root.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1838
docs/classes/root.sasjs.html
Normal file
1838
docs/classes/root.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1615
docs/classes/root.sasviyaapiclient.html
Normal file
1615
docs/classes/root.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
360
docs/classes/root.sessionmanager.html
Normal file
360
docs/classes/root.sessionmanager.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
215
docs/interfaces/types.polloptions.html
Normal file
215
docs/interfaces/types.polloptions.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
197
docs/interfaces/types.sessionvariable.html
Normal file
197
docs/interfaces/types.sessionvariable.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
106
docs/modules/reflection-790.html
Normal file
106
docs/modules/reflection-790.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-790.reflection-214.html
Normal file
128
docs/modules/reflection-790.reflection-214.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-804.html
Normal file
106
docs/modules/reflection-804.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-804.reflection-219.html
Normal file
128
docs/modules/reflection-804.reflection-219.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-831.html
Normal file
106
docs/modules/reflection-831.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-831.reflection-220.html
Normal file
128
docs/modules/reflection-831.reflection-220.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-837.html
Normal file
106
docs/modules/reflection-837.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-837.reflection-220.html
Normal file
128
docs/modules/reflection-837.reflection-220.html
Normal file
File diff suppressed because one or more lines are too long
129
docs/modules/root.html
Normal file
129
docs/modules/root.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
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,
|
||||
}
|
||||
|
||||
23819
package-lock.json
generated
23819
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -2,12 +2,12 @@
|
||||
"name": "@sasjs/adapter",
|
||||
"description": "JavaScript adapter for SAS",
|
||||
"scripts": {
|
||||
"build": "rimraf build && webpack",
|
||||
"build": "rimraf build && rimraf node && mkdir node && cp -r src/* node && webpack && rimraf build/src && rimraf node",
|
||||
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"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",
|
||||
"test": "jest --silent --coverage",
|
||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||
"postpublish": "git clean -fd",
|
||||
"semantic-release": "semantic-release",
|
||||
@@ -36,29 +36,31 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/isomorphic-fetch": "0.0.35",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/jest": "^26.0.20",
|
||||
"cp": "^0.2.0",
|
||||
"jest": "^25.5.4",
|
||||
"dotenv": "^8.2.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-extended": "^0.11.5",
|
||||
"path": "^0.12.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.1.2",
|
||||
"terser-webpack-plugin": "^4.2.2",
|
||||
"semantic-release": "^17.3.9",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
"ts-loader": "^8.0.17",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.17.8",
|
||||
"typedoc-neo-theme": "^1.0.10",
|
||||
"typedoc-plugin-external-module-name": "^4.0.3",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"typedoc": "^0.19.2",
|
||||
"typedoc-neo-theme": "^1.1.0",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "^3.9.9",
|
||||
"webpack": "^5.21.2",
|
||||
"webpack-cli": "^4.5.0"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.2.8",
|
||||
"axios": "^0.21.1",
|
||||
"@sasjs/utils": "^2.5.0",
|
||||
"form-data": "^3.0.0",
|
||||
"isomorphic-fetch": "^2.2.1"
|
||||
"https": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
12348
sasjs-tests/package-lock.json
generated
12348
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.12.0",
|
||||
"@sasjs/adapter": "^2.1.0",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
||||
test("renders learn react link", () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,15 +1,17 @@
|
||||
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 defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSAS9: "/SASStoredProcess/do",
|
||||
pathSASViya: "/SASJobExecution",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SASViya,
|
||||
debug: true,
|
||||
serverType: ServerType.SasViya,
|
||||
debug: false,
|
||||
contextName: "SAS Job Execution compute context",
|
||||
useComputeApi: false
|
||||
useComputeApi: false,
|
||||
allowInsecureRequests: false
|
||||
};
|
||||
|
||||
const customConfig = {
|
||||
@@ -17,7 +19,7 @@ const customConfig = {
|
||||
pathSAS9: "sas9",
|
||||
pathSASViya: "viya",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SAS9,
|
||||
serverType: ServerType.Sas9,
|
||||
debug: false
|
||||
};
|
||||
|
||||
@@ -37,6 +39,18 @@ export const basicTests = (
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: "Multiple Log in attempts",
|
||||
description:
|
||||
"Should fail on first attempt and should log the user in on second attempt",
|
||||
test: async () => {
|
||||
await adapter.logOut();
|
||||
await adapter.logIn("invalid", "invalid");
|
||||
return adapter.logIn(userName, password);
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: "Default config",
|
||||
description:
|
||||
@@ -46,6 +60,7 @@ export const basicTests = (
|
||||
},
|
||||
assertion: (sasjsInstance: SASjs) => {
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||
|
||||
return (
|
||||
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||
|
||||
@@ -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,21 +21,86 @@ 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);
|
||||
const expectedProperties = [
|
||||
"id",
|
||||
"state",
|
||||
"creationTimeStamp",
|
||||
"jobConditionCode"
|
||||
];
|
||||
return validate(expectedProperties, res.job);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Execute Script Viya - complete job",
|
||||
description: "Should execute sas file and return log",
|
||||
test: () => {
|
||||
const fileLines = [
|
||||
`data;`,
|
||||
`do x=1 to 100;`,
|
||||
`output;`,
|
||||
`end;`,
|
||||
`run;`
|
||||
];
|
||||
|
||||
return adapter.executeScriptSASViya(
|
||||
"sasCode.sas",
|
||||
fileLines,
|
||||
"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`;
|
||||
|
||||
return validateLog(expectedLogContent, res.log);
|
||||
}
|
||||
},
|
||||
{
|
||||
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);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`;
|
||||
|
||||
return validateLog(expectedLogContent, res.log);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const validateLog = (text: string, log: string): boolean => {
|
||||
const isValid = JSON.stringify(log).includes(text);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||
const actualProperties = Object.keys(data);
|
||||
|
||||
const isValid = expectedProperties.every(
|
||||
(property) => actualProperties.includes(property)
|
||||
);
|
||||
return isValid
|
||||
}
|
||||
const isValid = expectedProperties.every((property) =>
|
||||
actualProperties.includes(property)
|
||||
);
|
||||
return isValid;
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.body && !!error.body.message;
|
||||
return !!error && !!error.error && !!error.error.message;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -185,7 +185,8 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
};
|
||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => !!error && !!error.body && !!error.body.message
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: "Single string value",
|
||||
@@ -219,7 +220,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
.catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.body && !!error.body.message;
|
||||
return !!error && !!error.error && !!error.error.message;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,24 +23,23 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||
},
|
||||
{
|
||||
title: "Make error and capture log",
|
||||
description: "Should make an error and capture log",
|
||||
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)
|
||||
.then((res) => {
|
||||
//no action here, this request must throw error
|
||||
})
|
||||
.catch((err) => {
|
||||
let sasRequests = adapter.getSasRequests();
|
||||
let makeErrRequest =
|
||||
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;
|
||||
|
||||
resolve(!!makeErrRequest);
|
||||
});
|
||||
});
|
||||
if (!makeErrRequest) return false;
|
||||
|
||||
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"
|
||||
|
||||
450
src/ContextManager.ts
Normal file
450
src/ContextManager.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
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 = [
|
||||
'CAS Formats service compute context',
|
||||
'Data Mining compute context',
|
||||
'Import 9 service compute context',
|
||||
'SAS Job Execution compute context',
|
||||
'SAS Model Manager compute context',
|
||||
'SAS Studio compute context',
|
||||
'SAS Visual Forecasting compute context'
|
||||
]
|
||||
private defaultLauncherContexts = [
|
||||
'CAS Formats service launcher context',
|
||||
'Data Mining launcher context',
|
||||
'Import 9 service launcher context',
|
||||
'Job Flow Execution launcher context',
|
||||
'SAS Job Execution launcher context',
|
||||
'SAS Model Manager launcher context',
|
||||
'SAS Studio launcher context',
|
||||
'SAS Visual Forecasting launcher context'
|
||||
]
|
||||
|
||||
get getDefaultComputeContexts() {
|
||||
return this.defaultComputeContexts
|
||||
}
|
||||
get getDefaultLauncherContexts() {
|
||||
return this.defaultLauncherContexts
|
||||
}
|
||||
|
||||
constructor(private serverUrl: string, private requestClient: RequestClient) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
public async getComputeContexts(accessToken?: string) {
|
||||
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 : []
|
||||
|
||||
return contextsList.map((context: any) => ({
|
||||
createdBy: context.createdBy,
|
||||
id: context.id,
|
||||
name: context.name,
|
||||
version: context.version,
|
||||
attributes: {}
|
||||
}))
|
||||
}
|
||||
|
||||
public async getLauncherContexts(accessToken?: string) {
|
||||
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 : []
|
||||
|
||||
return contextsList.map((context: any) => ({
|
||||
createdBy: context.createdBy,
|
||||
id: context.id,
|
||||
name: context.name,
|
||||
version: context.version,
|
||||
attributes: {}
|
||||
}))
|
||||
}
|
||||
|
||||
public async createComputeContext(
|
||||
contextName: string,
|
||||
launchContextName: string,
|
||||
sharedAccountId: string,
|
||||
autoExecLines: string[],
|
||||
accessToken?: string,
|
||||
authorizedUsers?: string[]
|
||||
) {
|
||||
this.validateContextName(contextName)
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultComputeContexts,
|
||||
`Compute context '${contextName}' already exists.`
|
||||
)
|
||||
|
||||
const existingComputeContexts = await this.getComputeContexts(accessToken)
|
||||
|
||||
if (
|
||||
existingComputeContexts.find((context) => context.name === contextName)
|
||||
) {
|
||||
throw new Error(`Compute context '${contextName}' already exists.`)
|
||||
}
|
||||
|
||||
if (launchContextName) {
|
||||
if (!this.defaultLauncherContexts.includes(launchContextName)) {
|
||||
const launcherContexts = await this.getLauncherContexts(accessToken)
|
||||
|
||||
if (
|
||||
!launcherContexts.find(
|
||||
(context) => context.name === launchContextName
|
||||
)
|
||||
) {
|
||||
const description = `The launcher context for ${launchContextName}`
|
||||
const launchType = 'direct'
|
||||
|
||||
const newLauncherContext = await this.createLauncherContext(
|
||||
launchContextName,
|
||||
description,
|
||||
launchType,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw new Error(`Error while creating launcher context. ${err}`)
|
||||
})
|
||||
|
||||
if (newLauncherContext && newLauncherContext.name) {
|
||||
launchContextName = newLauncherContext.name
|
||||
} else {
|
||||
throw new Error('Error while creating launcher context.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
let attributes = { reuseServerProcesses: true } as object
|
||||
|
||||
if (sharedAccountId)
|
||||
attributes = { ...attributes, runServerAs: sharedAccountId }
|
||||
|
||||
const requestBody: any = {
|
||||
name: contextName,
|
||||
launchContext: {
|
||||
contextName: launchContextName || ''
|
||||
},
|
||||
attributes
|
||||
}
|
||||
|
||||
if (authorizedUsers && authorizedUsers.length) {
|
||||
requestBody['authorizedUsers'] = authorizedUsers
|
||||
} else {
|
||||
requestBody['authorizeAllAuthenticatedUsers'] = true
|
||||
}
|
||||
|
||||
if (autoExecLines) {
|
||||
requestBody.environment = { autoExecLines }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
public async createLauncherContext(
|
||||
contextName: string,
|
||||
description: string,
|
||||
launchType = 'direct',
|
||||
accessToken?: string
|
||||
) {
|
||||
if (!contextName) {
|
||||
throw new Error('Context name is required.')
|
||||
}
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultLauncherContexts,
|
||||
`Launcher context '${contextName}' already exists.`
|
||||
)
|
||||
|
||||
const existingLauncherContexts = await this.getLauncherContexts(accessToken)
|
||||
|
||||
if (
|
||||
existingLauncherContexts.find((context) => context.name === contextName)
|
||||
) {
|
||||
throw new Error(`Launcher context '${contextName}' already exists.`)
|
||||
}
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
name: contextName,
|
||||
description: description,
|
||||
launchType
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
public async editComputeContext(
|
||||
contextName: string,
|
||||
editedContext: EditContextInput,
|
||||
accessToken?: string
|
||||
) {
|
||||
this.validateContextName(contextName)
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultComputeContexts,
|
||||
'Editing default SAS compute contexts is not allowed.',
|
||||
true
|
||||
)
|
||||
|
||||
let originalContext
|
||||
|
||||
originalContext = await this.getComputeContextByName(
|
||||
contextName,
|
||||
accessToken
|
||||
)
|
||||
|
||||
// Try to find context by id, when context name has been changed.
|
||||
if (!originalContext) {
|
||||
originalContext = await this.getComputeContextById(
|
||||
editedContext.id!,
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
// 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
|
||||
return await this.requestClient.put<Context>(
|
||||
`/compute/contexts/${context.id}`,
|
||||
{
|
||||
...context,
|
||||
...editedContext,
|
||||
attributes: { ...context.attributes, ...editedContext.attributes }
|
||||
},
|
||||
accessToken,
|
||||
{ 'If-Match': etag }
|
||||
)
|
||||
}
|
||||
|
||||
public async getComputeContextByName(
|
||||
contextName: string,
|
||||
accessToken?: string
|
||||
): Promise<Context> {
|
||||
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(
|
||||
`The context '${contextName}' was not found at '${this.serverUrl}'.`
|
||||
)
|
||||
}
|
||||
|
||||
return contexts.items[0]
|
||||
}
|
||||
|
||||
public async getComputeContextById(
|
||||
contextId: string,
|
||||
accessToken?: string
|
||||
): Promise<ContextAllAttributes> {
|
||||
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
|
||||
}
|
||||
|
||||
public async getExecutableContexts(
|
||||
executeScript: Function,
|
||||
accessToken?: string
|
||||
) {
|
||||
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[] = []
|
||||
|
||||
const promises = contextsList.map((context: any) => {
|
||||
const linesOfCode = ['%put &=sysuserid;']
|
||||
|
||||
return () =>
|
||||
executeScript(
|
||||
`test-${context.name}`,
|
||||
linesOfCode,
|
||||
context.name,
|
||||
accessToken,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
).catch((err: any) => err)
|
||||
})
|
||||
|
||||
let results: any[] = []
|
||||
|
||||
for (const promise of promises) results.push(await promise())
|
||||
|
||||
results.forEach((result: any, index: number) => {
|
||||
if (result && result.log) {
|
||||
try {
|
||||
const resultParsed = result.log
|
||||
let sysUserId = ''
|
||||
|
||||
const sysUserIdLog = resultParsed
|
||||
.split('\n')
|
||||
.find((line: string) => line.startsWith('SYSUSERID='))
|
||||
|
||||
if (sysUserIdLog) {
|
||||
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
||||
|
||||
executableContexts.push({
|
||||
createdBy: contextsList[index].createdBy,
|
||||
id: contextsList[index].id,
|
||||
name: contextsList[index].name,
|
||||
version: contextsList[index].version,
|
||||
attributes: {
|
||||
sysUserId
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return executableContexts
|
||||
}
|
||||
|
||||
public async deleteComputeContext(contextName: string, accessToken?: string) {
|
||||
this.validateContextName(contextName)
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultComputeContexts,
|
||||
'Deleting default SAS compute contexts is not allowed.',
|
||||
true
|
||||
)
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const context = await this.getComputeContextByName(contextName, accessToken)
|
||||
|
||||
return await this.requestClient.delete<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: implement editLauncherContext method
|
||||
|
||||
// TODO: implement deleteLauncherContext method
|
||||
|
||||
private validateContextName(name: string) {
|
||||
if (!name) throw new Error('Context name is required.')
|
||||
}
|
||||
|
||||
public isDefaultContext(
|
||||
context: string,
|
||||
defaultContexts: string[] = this.defaultComputeContexts,
|
||||
errorMessage = '',
|
||||
listDefaults = false
|
||||
) {
|
||||
if (defaultContexts.includes(context)) {
|
||||
throw new Error(
|
||||
`${errorMessage}${
|
||||
listDefaults
|
||||
? '\nDefault contexts:' +
|
||||
defaultContexts.map((context, i) => `\n${i + 1}. ${context}`)
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
||||
import { CsrfToken } from './types/CsrfToken'
|
||||
import { isUrl } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
|
||||
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) {
|
||||
if (files?.length < 1)
|
||||
throw new Error('At least one file must be provided.')
|
||||
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 = ''
|
||||
|
||||
@@ -32,73 +32,39 @@ export class FileUploader {
|
||||
const program = this.appLoc
|
||||
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
|
||||
const uploadUrl = `${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)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData()
|
||||
const csrfToken = this.requestClient.getCsrfToken('file')
|
||||
if (csrfToken) formData.append('_csrf', csrfToken.value)
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
if (this.csrfToken) formData.append('_csrf', this.csrfToken.value)
|
||||
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
referrerPolicy: 'same-origin',
|
||||
headers
|
||||
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)
|
||||
)
|
||||
}
|
||||
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('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(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
1108
src/SASjs.ts
1108
src/SASjs.ts
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
import { Session, Context, CsrfToken } from './types'
|
||||
import { asyncForEach, makeRequest, isUrl } from './utils'
|
||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
||||
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
|
||||
@@ -13,15 +15,18 @@ 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,
|
||||
state: ''
|
||||
}
|
||||
|
||||
public get debug() {
|
||||
return this._debug
|
||||
@@ -53,15 +58,8 @@ 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)
|
||||
})
|
||||
@@ -93,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)
|
||||
|
||||
@@ -114,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
|
||||
@@ -161,33 +162,37 @@ 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, _) => {
|
||||
if (sessionState === 'pending') {
|
||||
if (
|
||||
sessionState === 'pending' ||
|
||||
sessionState === 'running' ||
|
||||
sessionState === ''
|
||||
) {
|
||||
if (stateLink) {
|
||||
if (this.debug) {
|
||||
console.log('Polling session status... \n') // ?
|
||||
if (this.debug && !this.printedSessionState.printed) {
|
||||
console.log('Polling session status...')
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
sessionState = state.trim()
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Current state is '${sessionState}'\n`)
|
||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||
console.log(`Current session state is '${sessionState}'`)
|
||||
|
||||
this.printedSessionState.state = sessionState
|
||||
this.printedSessionState.printed = false
|
||||
}
|
||||
|
||||
// There is an internal error present in SAS Viya 3.5
|
||||
@@ -209,56 +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
|
||||
})
|
||||
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
|
||||
})
|
||||
async getVariable(sessionId: string, variable: string, accessToken?: string) {
|
||||
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 }
|
||||
}
|
||||
54
src/job-execution/ComputeJobExecutor.ts
Normal file
54
src/job-execution/ComputeJobExecutor.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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
|
||||
|
||||
return this.sasViyaApiClient
|
||||
?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
waitForResult,
|
||||
expectWebout
|
||||
)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
let responseJson
|
||||
|
||||
return response.result
|
||||
|
||||
return responseJson
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof ComputeJobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
40
src/job-execution/JesJobExecutor.ts
Normal file
40
src/job-execution/JesJobExecutor.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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())
|
||||
return await this.sasViyaApiClient
|
||||
?.executeJob(sasJob, config.contextName, config.debug, data, accessToken)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
return response.result
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/job-execution/WebJobExecutor.ts
Normal file
189
src/job-execution/WebJobExecutor.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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 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])
|
||||
}
|
||||
}
|
||||
|
||||
return 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)
|
||||
return jsonResponse
|
||||
}
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
return res.result
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
}
|
||||
|
||||
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'
|
||||
464
src/request/RequestClient.ts
Normal file
464
src/request/RequestClient.ts
Normal file
@@ -0,0 +1,464 @@
|
||||
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'
|
||||
|
||||
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)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
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: '*/*' }
|
||||
})
|
||||
|
||||
if (isAuthorizeFormRequired(res?.data as string)) {
|
||||
await this.authorize(res.data as string)
|
||||
}
|
||||
return await callback()
|
||||
}
|
||||
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()
|
||||
}
|
||||
throw e
|
||||
} else if (response?.status === 404) {
|
||||
throw new NotFoundError(response.config.url!)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
private async parseResponse<T>(response: AxiosResponse<any>) {
|
||||
const etag = response?.headers ? response.headers['etag'] : ''
|
||||
let parsedResponse
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: parsedResponse as T,
|
||||
etag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
590
src/test/ContextManager.spec.ts
Normal file
590
src/test/ContextManager.spec.ts
Normal file
@@ -0,0 +1,590 @@
|
||||
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', () => {
|
||||
dotenv.config()
|
||||
|
||||
const contextManager = new ContextManager(
|
||||
process.env.SERVER_URL as string,
|
||||
new RequestClient(process.env.SERVER_URL as string)
|
||||
)
|
||||
|
||||
const defaultComputeContexts = contextManager.getDefaultComputeContexts
|
||||
const defaultLauncherContexts = contextManager.getDefaultLauncherContexts
|
||||
|
||||
const getRandomDefaultComputeContext = () =>
|
||||
defaultComputeContexts[
|
||||
Math.floor(Math.random() * defaultComputeContexts.length)
|
||||
]
|
||||
const getRandomDefaultLauncherContext = () =>
|
||||
defaultLauncherContexts[
|
||||
Math.floor(Math.random() * defaultLauncherContexts.length)
|
||||
]
|
||||
|
||||
describe('getComputeContexts', () => {
|
||||
it('should fetch compute contexts', async () => {
|
||||
const sampleComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Fake Compute Context',
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponse = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(contextManager.getComputeContexts()).resolves.toEqual([
|
||||
sampleComputeContext
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLauncherContexts', () => {
|
||||
it('should fetch launcher contexts', async () => {
|
||||
const sampleComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Fake Launcher Context',
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponse = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(contextManager.getLauncherContexts()).resolves.toEqual([
|
||||
sampleComputeContext
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('createComputeContext', () => {
|
||||
it('should throw an error if context name was not provided', async () => {
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
'',
|
||||
'Test Launcher Context',
|
||||
'fakeAccountId',
|
||||
[]
|
||||
)
|
||||
).rejects.toEqual(new Error('Context name is required.'))
|
||||
})
|
||||
|
||||
it('should throw an error when attempt to create context with reserved name', async () => {
|
||||
const contextName = getRandomDefaultComputeContext()
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
contextName,
|
||||
'Test Launcher Context',
|
||||
'fakeAccountId',
|
||||
[]
|
||||
)
|
||||
).rejects.toEqual(
|
||||
new Error(`Compute context '${contextName}' already exists.`)
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if context already exists', async () => {
|
||||
const contextName = 'Existing Compute Context'
|
||||
|
||||
const sampleComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: contextName,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponse = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
contextName,
|
||||
'Test Launcher Context',
|
||||
'fakeAccountId',
|
||||
[]
|
||||
)
|
||||
).rejects.toEqual(
|
||||
new Error(`Compute context '${contextName}' already exists.`)
|
||||
)
|
||||
})
|
||||
|
||||
it('should create compute context without launcher context', async () => {
|
||||
const contextName = 'New Compute Context'
|
||||
|
||||
const sampleExistingComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Existing Compute Context',
|
||||
attributes: {}
|
||||
}
|
||||
const sampleNewComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: contextName,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponseExistingComputeContexts = {
|
||||
items: [sampleExistingComputeContext]
|
||||
}
|
||||
const sampleResponseCreatedComputeContext = {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
contextName,
|
||||
'',
|
||||
'fakeAccountId',
|
||||
[]
|
||||
)
|
||||
).resolves.toEqual({
|
||||
items: [
|
||||
{
|
||||
attributes: {},
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
name: contextName,
|
||||
version: 2
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should create compute context with default launcher context', async () => {
|
||||
const contextName = 'New Compute Context'
|
||||
|
||||
const sampleExistingComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Existing Compute Context',
|
||||
attributes: {}
|
||||
}
|
||||
const sampleNewComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: contextName,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponseExistingComputeContexts = {
|
||||
items: [sampleExistingComputeContext]
|
||||
}
|
||||
const sampleResponseCreatedComputeContext = {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
contextName,
|
||||
getRandomDefaultLauncherContext(),
|
||||
'fakeAccountId',
|
||||
[]
|
||||
)
|
||||
).resolves.toEqual({
|
||||
items: [
|
||||
{
|
||||
attributes: {},
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
name: contextName,
|
||||
version: 2
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should create compute context with not existing launcher context', async () => {
|
||||
const computeContextName = 'New Compute Context'
|
||||
const launcherContextName = 'New Launcher Context'
|
||||
|
||||
const sampleExistingComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Existing Compute Context',
|
||||
attributes: {}
|
||||
}
|
||||
const sampleNewComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: computeContextName,
|
||||
attributes: {}
|
||||
}
|
||||
const sampleNewLauncherContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: launcherContextName,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponseExistingComputeContexts = {
|
||||
items: [sampleExistingComputeContext]
|
||||
}
|
||||
const sampleResponseCreatedLauncherContext = {
|
||||
items: [sampleNewLauncherContext]
|
||||
}
|
||||
const sampleResponseCreatedComputeContext = {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedLauncherContext })
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
computeContextName,
|
||||
launcherContextName,
|
||||
'fakeAccountId',
|
||||
[]
|
||||
)
|
||||
).resolves.toEqual({
|
||||
items: [
|
||||
{
|
||||
attributes: {},
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
name: computeContextName,
|
||||
version: 2
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createLauncherContext', () => {
|
||||
it('should throw an error if context name was not provided', async () => {
|
||||
await expect(
|
||||
contextManager.createLauncherContext('', 'Test Description')
|
||||
).rejects.toEqual(new Error('Context name is required.'))
|
||||
})
|
||||
|
||||
it('should throw an error when attempt to create context with reserved name', async () => {
|
||||
const contextName = getRandomDefaultLauncherContext()
|
||||
|
||||
await expect(
|
||||
contextManager.createLauncherContext(contextName, 'Test Description')
|
||||
).rejects.toEqual(
|
||||
new Error(`Launcher context '${contextName}' already exists.`)
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if context already exists', async () => {
|
||||
const contextName = 'Existing Launcher Context'
|
||||
|
||||
const sampleLauncherContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: contextName,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponse = {
|
||||
items: [sampleLauncherContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createLauncherContext(contextName, 'Test Description')
|
||||
).rejects.toEqual(
|
||||
new Error(`Launcher context '${contextName}' already exists.`)
|
||||
)
|
||||
})
|
||||
|
||||
it('should create launcher context', async () => {
|
||||
const contextName = 'New Launcher Context'
|
||||
|
||||
const sampleExistingLauncherContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Existing Launcher Context',
|
||||
attributes: {}
|
||||
}
|
||||
const sampleNewLauncherContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: contextName,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponseExistingLauncherContext = {
|
||||
items: [sampleExistingLauncherContext]
|
||||
}
|
||||
const sampleResponseCreatedLauncherContext = {
|
||||
items: [sampleNewLauncherContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingLauncherContext })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedLauncherContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createLauncherContext(contextName, 'Test Description')
|
||||
).resolves.toEqual({
|
||||
items: [
|
||||
{
|
||||
attributes: {},
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
name: contextName,
|
||||
version: 2
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('editComputeContext', () => {
|
||||
const editedContext = {
|
||||
name: 'updated name',
|
||||
description: 'updated description',
|
||||
id: 'someId'
|
||||
}
|
||||
|
||||
it('should throw an error if context name was not provided', async () => {
|
||||
await expect(
|
||||
contextManager.editComputeContext('', editedContext)
|
||||
).rejects.toEqual(new Error('Context name is required.'))
|
||||
})
|
||||
|
||||
it('should throw an error when attempt to edit context with reserved name', async () => {
|
||||
const contextName = getRandomDefaultComputeContext()
|
||||
|
||||
let editError: Error = { name: '', message: '' }
|
||||
|
||||
try {
|
||||
contextManager.isDefaultContext(
|
||||
contextName,
|
||||
defaultComputeContexts,
|
||||
'Editing default SAS compute contexts is not allowed.',
|
||||
true
|
||||
)
|
||||
} catch (error) {
|
||||
editError = error
|
||||
}
|
||||
|
||||
await expect(
|
||||
contextManager.editComputeContext(contextName, editedContext)
|
||||
).rejects.toEqual(editError)
|
||||
})
|
||||
|
||||
it('should edit context if founded by name', async () => {
|
||||
const sampleComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: editedContext.name,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponseGetComputeContextByName = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.put.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseGetComputeContextByName })
|
||||
)
|
||||
|
||||
const expectedResponse = {
|
||||
etag: '',
|
||||
result: sampleResponseGetComputeContextByName
|
||||
}
|
||||
|
||||
await expect(
|
||||
contextManager.editComputeContext(editedContext.name, editedContext)
|
||||
).resolves.toEqual(expectedResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getExecutableContexts', () => {
|
||||
it('should return executable contexts', async () => {
|
||||
const sampleComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Executable Compute Context',
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponse = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const user = 'testUser'
|
||||
|
||||
const fakedExecuteScript = async () => {
|
||||
return Promise.resolve({ log: `SYSUSERID=${user}` })
|
||||
}
|
||||
|
||||
const expectedResponse = [
|
||||
{
|
||||
...sampleComputeContext,
|
||||
attributes: { sysUserId: user }
|
||||
}
|
||||
]
|
||||
|
||||
await expect(
|
||||
contextManager.getExecutableContexts(fakedExecuteScript)
|
||||
).resolves.toEqual(expectedResponse)
|
||||
})
|
||||
|
||||
it('should not return executable contexts', async () => {
|
||||
const sampleComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: 'Not Executable Compute Context',
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponse = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const fakedExecuteScript = async () => {
|
||||
return Promise.resolve({ log: '' })
|
||||
}
|
||||
|
||||
await expect(
|
||||
contextManager.getExecutableContexts(fakedExecuteScript)
|
||||
).resolves.toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteComputeContext', () => {
|
||||
it('should throw an error if context name was not provided', async () => {
|
||||
await expect(contextManager.deleteComputeContext('')).rejects.toEqual(
|
||||
new Error('Context name is required.')
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error when attempt to delete context with reserved name', async () => {
|
||||
const contextName = getRandomDefaultComputeContext()
|
||||
|
||||
let deleteError: Error = { name: '', message: '' }
|
||||
|
||||
try {
|
||||
contextManager.isDefaultContext(
|
||||
contextName,
|
||||
defaultComputeContexts,
|
||||
'Deleting default SAS compute contexts is not allowed.',
|
||||
true
|
||||
)
|
||||
} catch (error) {
|
||||
deleteError = error
|
||||
}
|
||||
|
||||
await expect(
|
||||
contextManager.deleteComputeContext(contextName)
|
||||
).rejects.toEqual(deleteError)
|
||||
})
|
||||
|
||||
it('should delete context', async () => {
|
||||
const contextName = 'Compute Context To Delete'
|
||||
|
||||
const sampleComputeContext = {
|
||||
createdBy: 'fake creator',
|
||||
id: 'fakeId',
|
||||
version: 2,
|
||||
name: contextName,
|
||||
attributes: {}
|
||||
}
|
||||
|
||||
const sampleResponseGetComputeContextByName = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
const sampleResponseDeletedContext = {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseGetComputeContextByName })
|
||||
)
|
||||
|
||||
mockedAxios.delete.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseDeletedContext })
|
||||
)
|
||||
|
||||
const expectedResponse = {
|
||||
etag: '',
|
||||
result: sampleResponseDeletedContext
|
||||
}
|
||||
|
||||
await expect(
|
||||
contextManager.deleteComputeContext(contextName)
|
||||
).resolves.toEqual(expectedResponse)
|
||||
})
|
||||
})
|
||||
})
|
||||
116
src/test/FileUploader.spec.ts
Normal file
116
src/test/FileUploader.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
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",
|
||||
"_DEBUG":" ",
|
||||
"SYS_JES_JOB_URI": "/jobExecution/jobs/000-000-000-000",
|
||||
"_PROGRAM" : "/Public/app/editors/loadfile",
|
||||
"SYSCC" : "0",
|
||||
"SYSJOBID" : "117382",
|
||||
"SYSWARNINGTEXT" : ""
|
||||
}`
|
||||
|
||||
const prepareFilesAndParams = () => {
|
||||
const files: UploadFile[] = [
|
||||
{
|
||||
file: new File([''], 'testfile'),
|
||||
fileName: 'testfile'
|
||||
}
|
||||
]
|
||||
const params = { table: 'libtable' }
|
||||
|
||||
return { files, params }
|
||||
}
|
||||
|
||||
describe('FileUploader', () => {
|
||||
const fileUploader = new FileUploader(
|
||||
'/sample/apploc',
|
||||
'https://sample.server.com',
|
||||
'/jobs/path',
|
||||
new RequestClient('https://sample.server.com')
|
||||
)
|
||||
|
||||
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(res).toEqual(JSON.parse(sampleResponse))
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should an error when no files are provided', async (done) => {
|
||||
const sasJob = 'test/upload'
|
||||
const files: UploadFile[] = []
|
||||
const params = { table: 'libtable' }
|
||||
|
||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
||||
expect(err.error.message).toEqual('At least one file must be provided.')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error when no sasJob is provided', async (done) => {
|
||||
const sasJob = ''
|
||||
const { files, params } = prepareFilesAndParams()
|
||||
|
||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
||||
expect(err.error.message).toEqual('sasJob must be provided.')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error when login is required', async (done) => {
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<form action="Logon">' })
|
||||
)
|
||||
|
||||
const sasJob = 'test'
|
||||
const { files, params } = prepareFilesAndParams()
|
||||
|
||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
||||
expect(err.error.message).toEqual(
|
||||
'You must be logged in to upload a file.'
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error when invalid JSON is returned by the server', async (done) => {
|
||||
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('File upload request failed.')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error when the server request fails', async (done) => {
|
||||
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('File upload request failed.')
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
43
src/test/SessionManager.spec.ts
Normal file
43
src/test/SessionManager.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { SessionManager } from '../SessionManager'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import axios from 'axios'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('SessionManager', () => {
|
||||
dotenv.config()
|
||||
|
||||
const sessionManager = new SessionManager(
|
||||
process.env.SERVER_URL as string,
|
||||
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
||||
new RequestClient('https://sample.server.com')
|
||||
)
|
||||
|
||||
describe('getVariable', () => {
|
||||
it('should fetch session variable', async () => {
|
||||
const sampleResponse = {
|
||||
ok: true,
|
||||
links: [],
|
||||
name: 'SYSJOBID',
|
||||
scope: 'GLOBAL',
|
||||
value: '25218',
|
||||
version: 1
|
||||
}
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const expectedResponse = { etag: '', result: sampleResponse }
|
||||
|
||||
await expect(
|
||||
sessionManager.getVariable(
|
||||
'fakeSessionId',
|
||||
'SYSJOBID',
|
||||
'fakeAccessToken'
|
||||
)
|
||||
).resolves.toEqual(expectedResponse)
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user