([])
useEffect(() => {
if (adapter) {
@@ -20,15 +20,15 @@ const App = (): ReactElement<{}> => {
specialCaseTests(adapter),
sasjsRequestTests(adapter),
computeTests(adapter)
- ]);
+ ])
}
- }, [adapter, config]);
+ }, [adapter, config])
return (
{adapter && testSuites && }
- );
-};
+ )
+}
-export default App;
+export default App
diff --git a/sasjs-tests/src/Login.tsx b/sasjs-tests/src/Login.tsx
index 284f2d4..a72b458 100644
--- a/sasjs-tests/src/Login.tsx
+++ b/sasjs-tests/src/Login.tsx
@@ -1,22 +1,22 @@
-import React, { ReactElement, useState, useCallback, useContext } from "react";
-import "./Login.scss";
-import { AppContext } from "@sasjs/test-framework";
-import { Redirect } from "react-router-dom";
+import React, { ReactElement, useState, useCallback, useContext } from 'react'
+import './Login.scss'
+import { AppContext } from '@sasjs/test-framework'
+import { Redirect } from 'react-router-dom'
const Login = (): ReactElement<{}> => {
- const [username, setUsername] = useState("");
- const [password, setPassword] = useState("");
- const appContext = useContext(AppContext);
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const appContext = useContext(AppContext)
const handleSubmit = useCallback(
(e) => {
- e.preventDefault();
+ e.preventDefault()
appContext.adapter.logIn(username, password).then((res) => {
- appContext.setIsLoggedIn(res.isLoggedIn);
- });
+ appContext.setIsLoggedIn(res.isLoggedIn)
+ })
},
[username, password, appContext]
- );
+ )
return !appContext.isLoggedIn ? (
@@ -48,7 +48,7 @@ const Login = (): ReactElement<{}> => {
) : (
- );
-};
+ )
+}
-export default Login;
+export default Login
diff --git a/sasjs-tests/src/PrivateRoute.tsx b/sasjs-tests/src/PrivateRoute.tsx
index 8420955..b873dd9 100644
--- a/sasjs-tests/src/PrivateRoute.tsx
+++ b/sasjs-tests/src/PrivateRoute.tsx
@@ -1,23 +1,23 @@
-import React, { ReactElement, useContext, FunctionComponent } from "react";
-import { Redirect, Route } from "react-router-dom";
-import { AppContext } from "@sasjs/test-framework";
+import React, { ReactElement, useContext, FunctionComponent } from 'react'
+import { Redirect, Route } from 'react-router-dom'
+import { AppContext } from '@sasjs/test-framework'
interface PrivateRouteProps {
- component: FunctionComponent;
- exact?: boolean;
- path: string;
+ component: FunctionComponent
+ exact?: boolean
+ path: string
}
const PrivateRoute = (
props: PrivateRouteProps
): ReactElement => {
- const { component, path, exact } = props;
- const appContext = useContext(AppContext);
+ const { component, path, exact } = props
+ const appContext = useContext(AppContext)
return appContext.isLoggedIn ? (
) : (
- );
-};
+ )
+}
-export default PrivateRoute;
+export default PrivateRoute
diff --git a/sasjs-tests/src/index.tsx b/sasjs-tests/src/index.tsx
index effc537..3b51af1 100644
--- a/sasjs-tests/src/index.tsx
+++ b/sasjs-tests/src/index.tsx
@@ -1,12 +1,12 @@
-import React from "react";
-import ReactDOM from "react-dom";
-import { Route, HashRouter, Switch } from "react-router-dom";
-import "./index.scss";
-import * as serviceWorker from "./serviceWorker";
-import { AppProvider } from "@sasjs/test-framework";
-import PrivateRoute from "./PrivateRoute";
-import Login from "./Login";
-import App from "./App";
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { Route, HashRouter, Switch } from 'react-router-dom'
+import './index.scss'
+import * as serviceWorker from './serviceWorker'
+import { AppProvider } from '@sasjs/test-framework'
+import PrivateRoute from './PrivateRoute'
+import Login from './Login'
+import App from './App'
ReactDOM.render(
@@ -17,10 +17,10 @@ ReactDOM.render(
,
- document.getElementById("root")
-);
+ document.getElementById('root')
+)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.unregister();
+serviceWorker.unregister()
diff --git a/sasjs-tests/src/serviceWorker.js b/sasjs-tests/src/serviceWorker.js
index 58bd4c6..493867a 100644
--- a/sasjs-tests/src/serviceWorker.js
+++ b/sasjs-tests/src/serviceWorker.js
@@ -11,46 +11,46 @@
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
- window.location.hostname === "localhost" ||
+ window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
- window.location.hostname === "[::1]" ||
+ window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
-);
+)
export function register(config) {
- if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
+ return
}
- window.addEventListener("load", () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
+ checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
- "This web app is being served cache-first by a service " +
- "worker. To learn more, visit https://bit.ly/CRA-PWA"
- );
- });
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://bit.ly/CRA-PWA'
+ )
+ })
} else {
// Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
+ registerValidSW(swUrl, config)
}
- });
+ })
}
}
@@ -59,83 +59,83 @@ function registerValidSW(swUrl, config) {
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
- const installingWorker = registration.installing;
+ const installingWorker = registration.installing
if (installingWorker == null) {
- return;
+ return
}
installingWorker.onstatechange = () => {
- if (installingWorker.state === "installed") {
+ if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
- "New content is available and will be used when all " +
- "tabs for this page are closed. See https://bit.ly/CRA-PWA."
- );
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+ )
// Execute callback
if (config && config.onUpdate) {
- config.onUpdate(registration);
+ config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
- console.log("Content is cached for offline use.");
+ console.log('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
- config.onSuccess(registration);
+ config.onSuccess(registration)
}
}
}
- };
- };
+ }
+ }
})
.catch((error) => {
- console.error("Error during service worker registration:", error);
- });
+ console.error('Error during service worker registration:', error)
+ })
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
- headers: { "Service-Worker": "script" }
+ headers: { 'Service-Worker': 'script' }
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get("content-type");
+ const contentType = response.headers.get('content-type')
if (
response.status === 404 ||
- (contentType != null && contentType.indexOf("javascript") === -1)
+ (contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
- window.location.reload();
- });
- });
+ window.location.reload()
+ })
+ })
} else {
// Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
+ registerValidSW(swUrl, config)
}
})
.catch(() => {
console.log(
- "No internet connection found. App is running in offline mode."
- );
- });
+ 'No internet connection found. App is running in offline mode.'
+ )
+ })
}
export function unregister() {
- if ("serviceWorker" in navigator) {
+ if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
- registration.unregister();
+ registration.unregister()
})
.catch((error) => {
- console.error(error.message);
- });
+ console.error(error.message)
+ })
}
}
diff --git a/sasjs-tests/src/setupTests.js b/sasjs-tests/src/setupTests.js
index 5fdf001..2eb59b0 100644
--- a/sasjs-tests/src/setupTests.js
+++ b/sasjs-tests/src/setupTests.js
@@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
-import "@testing-library/jest-dom/extend-expect";
+import '@testing-library/jest-dom/extend-expect'
diff --git a/sasjs-tests/src/testSuites/Basic.ts b/sasjs-tests/src/testSuites/Basic.ts
index f7faf73..e1b5887 100644
--- a/sasjs-tests/src/testSuites/Basic.ts
+++ b/sasjs-tests/src/testSuites/Basic.ts
@@ -1,97 +1,102 @@
-import SASjs, { SASjsConfig } from "@sasjs/adapter";
-import { TestSuite } from "@sasjs/test-framework";
-import { ServerType } from "@sasjs/utils/types";
+import SASjs, { SASjsConfig } from '@sasjs/adapter'
+import { TestSuite } from '@sasjs/test-framework'
+import { ServerType } from '@sasjs/utils/types'
-const stringData: any = { table1: [{ col1: "first col value" }] };
+const stringData: any = { table1: [{ col1: 'first col value' }] }
const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin,
- pathSAS9: "/SASStoredProcess/do",
- pathSASViya: "/SASJobExecution",
- appLoc: "/Public/seedapp",
+ pathSAS9: '/SASStoredProcess/do',
+ pathSASViya: '/SASJobExecution',
+ appLoc: '/Public/seedapp',
serverType: ServerType.SasViya,
debug: false,
- contextName: "SAS Job Execution compute context",
+ contextName: 'SAS Job Execution compute context',
useComputeApi: false,
allowInsecureRequests: false
-};
+}
const customConfig = {
- serverUrl: "http://url.com",
- pathSAS9: "sas9",
- pathSASViya: "viya",
- appLoc: "/Public/seedapp",
+ serverUrl: 'http://url.com',
+ pathSAS9: 'sas9',
+ pathSASViya: 'viya',
+ appLoc: '/Public/seedapp',
serverType: ServerType.Sas9,
debug: false
-};
+}
export const basicTests = (
adapter: SASjs,
userName: string,
password: string
): TestSuite => ({
- name: "Basic Tests",
+ name: 'Basic Tests',
tests: [
{
- title: "Log in",
- description: "Should log the user in",
+ title: 'Log in',
+ description: 'Should log the user in',
test: async () => {
- return adapter.logIn(userName, password);
+ return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
- title: "Multiple Log in attempts",
+ title: 'Multiple Log in attempts',
description:
- "Should fail on first attempt and should log the user in on second attempt",
+ 'Should fail on first attempt and should log the user in on second attempt',
test: async () => {
- await adapter.logOut();
- await adapter.logIn("invalid", "invalid");
- return adapter.logIn(userName, password);
+ await adapter.logOut()
+ await adapter.logIn('invalid', 'invalid')
+ return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
- title: "Trigger login callback",
+ title: 'Trigger login callback',
description:
- "Should trigger required login callback and after successful login, it should finish the request",
+ 'Should trigger required login callback and after successful login, it should finish the request',
test: async () => {
- await adapter.logOut();
-
- return await adapter.request("common/sendArr", stringData, null, () => {
- adapter.logIn(userName, password);
- });
+ await adapter.logOut()
+
+ return await adapter.request(
+ 'common/sendArr',
+ stringData,
+ undefined,
+ () => {
+ adapter.logIn(userName, password)
+ }
+ )
},
assertion: (response: any) => {
- return response.table1[0][0] === stringData.table1[0].col1;
+ return response.table1[0][0] === stringData.table1[0].col1
}
},
{
- title: "Request with debug on",
+ title: 'Request with debug on',
description:
- "Should complete successful request with debugging switched on",
+ 'Should complete successful request with debugging switched on',
test: async () => {
const config = {
debug: true
}
- return await adapter.request("common/sendArr", stringData, config)
+ return await adapter.request('common/sendArr', stringData, config)
},
assertion: (response: any) => {
- return response.table1[0][0] === stringData.table1[0].col1;
+ return response.table1[0][0] === stringData.table1[0].col1
}
},
{
- title: "Default config",
+ title: 'Default config',
description:
- "Should instantiate with default config when none is provided",
+ 'Should instantiate with default config when none is provided',
test: async () => {
- return Promise.resolve(new SASjs());
+ return Promise.resolve(new SASjs())
},
assertion: (sasjsInstance: SASjs) => {
- const sasjsConfig = sasjsInstance.getSasjsConfig();
+ const sasjsConfig = sasjsInstance.getSasjsConfig()
return (
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
@@ -100,17 +105,17 @@ export const basicTests = (
sasjsConfig.appLoc === defaultConfig.appLoc &&
sasjsConfig.serverType === defaultConfig.serverType &&
sasjsConfig.debug === defaultConfig.debug
- );
+ )
}
},
{
- title: "Custom config",
- description: "Should use fully custom config whenever supplied",
+ title: 'Custom config',
+ description: 'Should use fully custom config whenever supplied',
test: async () => {
- return Promise.resolve(new SASjs(customConfig));
+ return Promise.resolve(new SASjs(customConfig))
},
assertion: (sasjsInstance: SASjs) => {
- const sasjsConfig = sasjsInstance.getSasjsConfig();
+ const sasjsConfig = sasjsInstance.getSasjsConfig()
return (
sasjsConfig.serverUrl === customConfig.serverUrl &&
sasjsConfig.pathSAS9 === customConfig.pathSAS9 &&
@@ -118,28 +123,28 @@ export const basicTests = (
sasjsConfig.appLoc === customConfig.appLoc &&
sasjsConfig.serverType === customConfig.serverType &&
sasjsConfig.debug === customConfig.debug
- );
+ )
}
},
{
- title: "Config overrides",
- description: "Should override default config with supplied properties",
+ title: 'Config overrides',
+ description: 'Should override default config with supplied properties',
test: async () => {
return Promise.resolve(
- new SASjs({ serverUrl: "http://test.com", debug: false })
- );
+ new SASjs({ serverUrl: 'http://test.com', debug: false })
+ )
},
assertion: (sasjsInstance: SASjs) => {
- const sasjsConfig = sasjsInstance.getSasjsConfig();
+ const sasjsConfig = sasjsInstance.getSasjsConfig()
return (
- sasjsConfig.serverUrl === "http://test.com" &&
+ sasjsConfig.serverUrl === 'http://test.com' &&
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
sasjsConfig.pathSASViya === defaultConfig.pathSASViya &&
sasjsConfig.appLoc === defaultConfig.appLoc &&
sasjsConfig.serverType === defaultConfig.serverType &&
sasjsConfig.debug === false
- );
+ )
}
}
]
-});
+})
diff --git a/sasjs-tests/src/testSuites/Compute.ts b/sasjs-tests/src/testSuites/Compute.ts
index b2cf050..afbd73b 100644
--- a/sasjs-tests/src/testSuites/Compute.ts
+++ b/sasjs-tests/src/testSuites/Compute.ts
@@ -1,106 +1,100 @@
-import SASjs from "@sasjs/adapter";
-import { TestSuite } from "@sasjs/test-framework";
+import SASjs from '@sasjs/adapter'
+import { TestSuite } from '@sasjs/test-framework'
export const computeTests = (adapter: SASjs): TestSuite => ({
- name: "Compute",
+ name: 'Compute',
tests: [
{
- title: "Start Compute Job - not waiting for result",
- description: "Should start a compute job and return the session",
+ title: 'Start Compute Job - not waiting for result',
+ description: 'Should start a compute job and return the session',
test: () => {
- const data: any = { table1: [{ col1: "first col value" }] };
- return adapter.startComputeJob("/Public/app/common/sendArr", data);
+ const data: any = { table1: [{ col1: 'first col value' }] }
+ return adapter.startComputeJob('/Public/app/common/sendArr', data)
},
assertion: (res: any) => {
- const expectedProperties = ["id", "applicationName", "attributes"];
- return validate(expectedProperties, res);
+ const expectedProperties = ['id', 'applicationName', 'attributes']
+ return validate(expectedProperties, res)
}
},
{
- title: "Start Compute Job - waiting for result",
- description: "Should start a compute job and return the job",
+ title: 'Start Compute Job - waiting for result',
+ description: 'Should start a compute job and return the job',
test: () => {
- const data: any = { table1: [{ col1: "first col value" }] };
+ const data: any = { table1: [{ col1: 'first col value' }] }
return adapter.startComputeJob(
- "/Public/app/common/sendArr",
+ '/Public/app/common/sendArr',
data,
{},
- "",
+ '',
true
- );
+ )
},
assertion: (res: any) => {
const expectedProperties = [
- "id",
- "state",
- "creationTimeStamp",
- "jobConditionCode"
- ];
- return validate(expectedProperties, res.job);
+ 'id',
+ 'state',
+ 'creationTimeStamp',
+ 'jobConditionCode'
+ ]
+ return validate(expectedProperties, res.job)
}
},
{
- title: "Execute Script Viya - complete job",
- description: "Should execute sas file and return log",
+ 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;`
- ];
+ const fileLines = [`data;`, `do x=1 to 100;`, `output;`, `end;`, `run;`]
return adapter.executeScriptSASViya(
- "sasCode.sas",
+ 'sasCode.sas',
fileLines,
- "SAS Studio compute context",
+ 'SAS Studio compute context',
undefined,
true
- );
+ )
},
assertion: (res: any) => {
- const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`;
+ const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
- return validateLog(expectedLogContent, res.log);
+ return validateLog(expectedLogContent, res.log)
}
},
{
- title: "Execute Script Viya - failed job",
- description: "Should execute sas file and return log",
+ title: 'Execute Script Viya - failed job',
+ description: 'Should execute sas file and return log',
test: () => {
- const fileLines = [`%abort;`];
+ const fileLines = [`%abort;`]
return adapter
.executeScriptSASViya(
- "sasCode.sas",
+ 'sasCode.sas',
fileLines,
- "SAS Studio compute context",
+ 'SAS Studio compute context',
undefined,
true
)
- .catch((err: any) => err);
+ .catch((err: any) => err)
},
assertion: (res: any) => {
- const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`;
+ const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
- return validateLog(expectedLogContent, res.log);
+ return validateLog(expectedLogContent, res.log)
}
}
]
-});
+})
const validateLog = (text: string, log: string): boolean => {
- const isValid = JSON.stringify(log).includes(text);
+ const isValid = JSON.stringify(log).includes(text)
- return isValid;
-};
+ return isValid
+}
const validate = (expectedProperties: string[], data: any): boolean => {
- const actualProperties = Object.keys(data);
+ const actualProperties = Object.keys(data)
const isValid = expectedProperties.every((property) =>
actualProperties.includes(property)
- );
- return isValid;
-};
+ )
+ return isValid
+}
diff --git a/sasjs-tests/src/testSuites/RequestData.ts b/sasjs-tests/src/testSuites/RequestData.ts
index 24c33fb..a2088f3 100644
--- a/sasjs-tests/src/testSuites/RequestData.ts
+++ b/sasjs-tests/src/testSuites/RequestData.ts
@@ -1,111 +1,112 @@
-import SASjs from "@sasjs/adapter";
-import { TestSuite } from "@sasjs/test-framework";
+import SASjs from '@sasjs/adapter'
+import { TestSuite } from '@sasjs/test-framework'
-const stringData: any = { table1: [{ col1: "first col value" }] };
-const numericData: any = { table1: [{ col1: 3.14159265 }] };
+const stringData: any = { table1: [{ col1: 'first col value' }] }
+const numericData: any = { table1: [{ col1: 3.14159265 }] }
const multiColumnData: any = {
- table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }]
-};
+ table1: [{ col1: 42, col2: 1.618, col3: 'x', col4: 'x' }]
+}
const multipleRowsWithNulls: any = {
table1: [
- { col1: 42, col2: null, col3: "x", col4: "" },
- { col1: 42, col2: null, col3: "x", col4: "" },
- { col1: 42, col2: null, col3: "x", col4: "" },
- { col1: 42, col2: 1.62, col3: "x", col4: "x" },
- { col1: 42, col2: 1.62, col3: "x", col4: "x" }
+ { col1: 42, col2: null, col3: 'x', col4: '' },
+ { col1: 42, col2: null, col3: 'x', col4: '' },
+ { col1: 42, col2: null, col3: 'x', col4: '' },
+ { col1: 42, col2: 1.62, col3: 'x', col4: 'x' },
+ { col1: 42, col2: 1.62, col3: 'x', col4: 'x' }
]
-};
+}
+
const multipleColumnsWithNulls: any = {
table1: [
- { col1: 42, col2: null, col3: "x", col4: null },
- { col1: 42, col2: null, col3: "x", col4: null },
- { col1: 42, col2: null, col3: "x", col4: null },
- { col1: 42, col2: null, col3: "x", col4: "" },
- { col1: 42, col2: null, col3: "x", col4: "" }
+ { col1: 42, col2: null, col3: 'x', col4: null },
+ { col1: 42, col2: null, col3: 'x', col4: null },
+ { col1: 42, col2: null, col3: 'x', col4: null },
+ { col1: 42, col2: null, col3: 'x', col4: '' },
+ { col1: 42, col2: null, col3: 'x', col4: '' }
]
-};
+}
const getLongStringData = (length = 32764) => {
- let x = "X";
+ let x = 'X'
for (let i = 1; i <= length; i++) {
- x = x + "X";
+ x = x + 'X'
}
- const data: any = { table1: [{ col1: x }] };
- return data;
-};
+ const data: any = { table1: [{ col1: x }] }
+ return data
+}
const getLargeObjectData = () => {
- const data = { table1: [{ big: "data" }] };
+ const data = { table1: [{ big: 'data' }] }
for (let i = 1; i < 10000; i++) {
- data.table1.push(data.table1[0]);
+ data.table1.push(data.table1[0])
}
- return data;
-};
+ return data
+}
export const sendArrTests = (adapter: SASjs): TestSuite => ({
- name: "sendArr",
+ name: 'sendArr',
tests: [
{
- title: "Absolute paths",
- description: "Should work with absolute paths to SAS jobs",
+ title: 'Absolute paths',
+ description: 'Should work with absolute paths to SAS jobs',
test: () => {
- return adapter.request("/Public/app/common/sendArr", stringData);
+ return adapter.request('/Public/app/common/sendArr', stringData)
},
assertion: (res: any) => {
- return res.table1[0][0] === stringData.table1[0].col1;
+ return res.table1[0][0] === stringData.table1[0].col1
}
},
{
- title: "Single string value",
- description: "Should send an array with a single string value",
+ title: 'Single string value',
+ description: 'Should send an array with a single string value',
test: () => {
- return adapter.request("common/sendArr", stringData);
+ return adapter.request('common/sendArr', stringData)
},
assertion: (res: any) => {
- return res.table1[0][0] === stringData.table1[0].col1;
+ return res.table1[0][0] === stringData.table1[0].col1
}
},
{
- title: "Long string value",
+ title: 'Long string value',
description:
- "Should send an array with a long string value under 32765 characters",
+ 'Should send an array with a long string value under 32765 characters',
test: () => {
- return adapter.request("common/sendArr", getLongStringData());
+ return adapter.request('common/sendArr', getLongStringData())
},
assertion: (res: any) => {
- const longStringData = getLongStringData();
- return res.table1[0][0] === longStringData.table1[0].col1;
+ const longStringData = getLongStringData()
+ return res.table1[0][0] === longStringData.table1[0].col1
}
},
{
- title: "Overly long string value",
+ title: 'Overly long string value',
description:
- "Should error out with long string values over 32765 characters",
+ 'Should error out with long string values over 32765 characters',
test: () => {
- const data = getLongStringData(32767);
- return adapter.request("common/sendArr", data).catch((e) => e);
+ const data = getLongStringData(32767)
+ return adapter.request('common/sendArr', data).catch((e) => e)
},
assertion: (error: any) => {
- return !!error && !!error.error && !!error.error.message;
+ return !!error && !!error.error && !!error.error.message
}
},
{
- title: "Single numeric value",
- description: "Should send an array with a single numeric value",
+ title: 'Single numeric value',
+ description: 'Should send an array with a single numeric value',
test: () => {
- return adapter.request("common/sendArr", numericData);
+ return adapter.request('common/sendArr', numericData)
},
assertion: (res: any) => {
- return res.table1[0][0] === numericData.table1[0].col1;
+ return res.table1[0][0] === numericData.table1[0].col1
}
},
{
- title: "Multiple columns",
- description: "Should handle data with multiple columns",
+ title: 'Multiple columns',
+ description: 'Should handle data with multiple columns',
test: () => {
- return adapter.request("common/sendArr", multiColumnData);
+ return adapter.request('common/sendArr', multiColumnData)
},
assertion: (res: any) => {
return (
@@ -113,143 +114,141 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
res.table1[0][1] === multiColumnData.table1[0].col2 &&
res.table1[0][2] === multiColumnData.table1[0].col3 &&
res.table1[0][3] === multiColumnData.table1[0].col4
- );
+ )
}
},
{
- title: "Multiple rows with nulls",
- description: "Should handle data with multiple rows with null values",
+ title: 'Multiple rows with nulls',
+ description: 'Should handle data with multiple rows with null values',
test: () => {
- return adapter.request("common/sendArr", multipleRowsWithNulls);
+ return adapter.request('common/sendArr', multipleRowsWithNulls)
},
assertion: (res: any) => {
- let result = true;
+ let result = true
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
result =
result &&
- res.table1[index][0] === multipleRowsWithNulls.table1[index].col1;
+ res.table1[index][0] === multipleRowsWithNulls.table1[index].col1
result =
result &&
- res.table1[index][1] === multipleRowsWithNulls.table1[index].col2;
+ res.table1[index][1] === multipleRowsWithNulls.table1[index].col2
result =
result &&
- res.table1[index][2] === multipleRowsWithNulls.table1[index].col3;
- result =
- result &&
- res.table1[index][3] === multipleRowsWithNulls.table1[index].col4;
- });
- return result;
- }
- },
- {
- title: "Multiple columns with nulls",
- description: "Should handle data with multiple columns with null values",
- test: () => {
- return adapter.request("common/sendArr", multipleColumnsWithNulls);
- },
- assertion: (res: any) => {
- let result = true;
- multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
- result =
- result &&
- res.table1[index][0] ===
- multipleColumnsWithNulls.table1[index].col1;
- result =
- result &&
- res.table1[index][1] ===
- multipleColumnsWithNulls.table1[index].col2;
- result =
- result &&
- res.table1[index][2] ===
- multipleColumnsWithNulls.table1[index].col3;
+ res.table1[index][2] === multipleRowsWithNulls.table1[index].col3
result =
result &&
res.table1[index][3] ===
- (multipleColumnsWithNulls.table1[index].col4 || "");
- });
- return result;
+ (multipleRowsWithNulls.table1[index].col4 || ' ')
+ })
+ return result
+ }
+ },
+ {
+ title: 'Multiple columns with nulls',
+ description: 'Should handle data with multiple columns with null values',
+ test: () => {
+ return adapter.request('common/sendArr', multipleColumnsWithNulls)
+ },
+ assertion: (res: any) => {
+ let result = true
+ multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
+ result =
+ result &&
+ res.table1[index][0] === multipleColumnsWithNulls.table1[index].col1
+ result =
+ result &&
+ res.table1[index][1] === multipleColumnsWithNulls.table1[index].col2
+ result =
+ result &&
+ res.table1[index][2] === multipleColumnsWithNulls.table1[index].col3
+ result =
+ result &&
+ res.table1[index][3] ===
+ (multipleColumnsWithNulls.table1[index].col4 || ' ')
+ })
+ return result
}
}
]
-});
+})
export const sendObjTests = (adapter: SASjs): TestSuite => ({
- name: "sendObj",
+ name: 'sendObj',
tests: [
{
- title: "Invalid column name",
- description: "Should throw an error",
+ title: 'Invalid column name',
+ description: 'Should throw an error',
test: async () => {
const invalidData: any = {
- "1 invalid table": [{ col1: 42 }]
- };
- return adapter.request("common/sendObj", invalidData).catch((e) => e);
+ '1 invalid table': [{ col1: 42 }]
+ }
+ return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
- title: "Single string value",
- description: "Should send an object with a single string value",
+ title: 'Single string value',
+ description: 'Should send an object with a single string value',
test: () => {
- return adapter.request("common/sendObj", stringData);
+ return adapter.request('common/sendObj', stringData)
},
assertion: (res: any) => {
- return res.table1[0].COL1 === stringData.table1[0].col1;
+ return res.table1[0].COL1 === stringData.table1[0].col1
}
},
{
- title: "Long string value",
+ title: 'Long string value',
description:
- "Should send an object with a long string value under 32765 characters",
+ 'Should send an object with a long string value under 32765 characters',
test: () => {
- return adapter.request("common/sendObj", getLongStringData());
+ return adapter.request('common/sendObj', getLongStringData())
},
assertion: (res: any) => {
- const longStringData = getLongStringData();
- return res.table1[0].COL1 === longStringData.table1[0].col1;
+ const longStringData = getLongStringData()
+ return res.table1[0].COL1 === longStringData.table1[0].col1
}
},
{
- title: "Overly long string value",
+ title: 'Overly long string value',
description:
- "Should error out with long string values over 32765 characters",
+ 'Should error out with long string values over 32765 characters',
test: () => {
return adapter
- .request("common/sendObj", getLongStringData(32767))
- .catch((e) => e);
+ .request('common/sendObj', getLongStringData(32767))
+ .catch((e) => e)
},
assertion: (error: any) => {
- return !!error && !!error.error && !!error.error.message;
+ return !!error && !!error.error && !!error.error.message
}
},
{
- title: "Single numeric value",
- description: "Should send an object with a single numeric value",
+ title: 'Single numeric value',
+ description: 'Should send an object with a single numeric value',
test: () => {
- return adapter.request("common/sendObj", numericData);
+ return adapter.request('common/sendObj', numericData)
},
assertion: (res: any) => {
- return res.table1[0].COL1 === numericData.table1[0].col1;
+ return res.table1[0].COL1 === numericData.table1[0].col1
}
},
{
- title: "Large data volume",
- description: "Should send an object with a large amount of data",
+ title: 'Large data volume',
+ description: 'Should send an object with a large amount of data',
test: () => {
- return adapter.request("common/sendObj", getLargeObjectData());
+ return adapter.request('common/sendObj', getLargeObjectData())
},
assertion: (res: any) => {
- const data = getLargeObjectData();
- return res.table1[9000].BIG === data.table1[9000].big;
+ const data = getLargeObjectData()
+ return res.table1[9000].BIG === data.table1[9000].big
}
},
{
- title: "Multiple columns",
- description: "Should handle data with multiple columns",
+ title: 'Multiple columns',
+ description: 'Should handle data with multiple columns',
test: () => {
- return adapter.request("common/sendObj", multiColumnData);
+ return adapter.request('common/sendObj', multiColumnData)
},
assertion: (res: any) => {
return (
@@ -257,62 +256,63 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
res.table1[0].COL3 === multiColumnData.table1[0].col3 &&
res.table1[0].COL4 === multiColumnData.table1[0].col4
- );
+ )
}
},
{
- title: "Multiple rows with nulls",
- description: "Should handle data with multiple rows with null values",
+ title: 'Multiple rows with nulls',
+ description: 'Should handle data with multiple rows with null values',
test: () => {
- return adapter.request("common/sendObj", multipleRowsWithNulls);
+ return adapter.request('common/sendObj', multipleRowsWithNulls)
},
assertion: (res: any) => {
- let result = true;
+ let result = true
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
result =
result &&
- res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1;
+ res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1
result =
result &&
- res.table1[index].COL2 === multipleRowsWithNulls.table1[index].col2;
+ res.table1[index].COL2 === multipleRowsWithNulls.table1[index].col2
result =
result &&
- res.table1[index].COL3 === multipleRowsWithNulls.table1[index].col3;
+ res.table1[index].COL3 === multipleRowsWithNulls.table1[index].col3
result =
result &&
- res.table1[index].COL4 === multipleRowsWithNulls.table1[index].col4;
- });
- return result;
+ res.table1[index].COL4 ===
+ (multipleRowsWithNulls.table1[index].col4 || ' ')
+ })
+ return result
}
},
{
- title: "Multiple columns with nulls",
- description: "Should handle data with multiple columns with null values",
+ title: 'Multiple columns with nulls',
+ description: 'Should handle data with multiple columns with null values',
test: () => {
- return adapter.request("common/sendObj", multipleColumnsWithNulls);
+ return adapter.request('common/sendObj', multipleColumnsWithNulls)
},
assertion: (res: any) => {
- let result = true;
+ let result = true
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
result =
result &&
res.table1[index].COL1 ===
- multipleColumnsWithNulls.table1[index].col1;
+ multipleColumnsWithNulls.table1[index].col1
result =
result &&
res.table1[index].COL2 ===
- multipleColumnsWithNulls.table1[index].col2;
+ multipleColumnsWithNulls.table1[index].col2
result =
result &&
res.table1[index].COL3 ===
- multipleColumnsWithNulls.table1[index].col3;
+ multipleColumnsWithNulls.table1[index].col3
result =
result &&
res.table1[index].COL4 ===
- (multipleColumnsWithNulls.table1[index].col4 || "");
- });
- return result;
+ (multipleColumnsWithNulls.table1[index].col4 || ' ')
+ })
+ return result
}
}
]
-});
+})
diff --git a/sasjs-tests/src/testSuites/SasjsRequests.ts b/sasjs-tests/src/testSuites/SasjsRequests.ts
index 2f0b75d..baa131b 100644
--- a/sasjs-tests/src/testSuites/SasjsRequests.ts
+++ b/sasjs-tests/src/testSuites/SasjsRequests.ts
@@ -1,49 +1,49 @@
-import SASjs from "@sasjs/adapter";
-import { TestSuite } from "@sasjs/test-framework";
+import SASjs from '@sasjs/adapter'
+import { TestSuite } from '@sasjs/test-framework'
-const data: any = { table1: [{ col1: "first col value" }] };
+const data: any = { table1: [{ col1: 'first col value' }] }
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
- name: "SASjs Requests",
+ name: 'SASjs Requests',
tests: [
{
- title: "WORK tables",
- description: "Should get WORK tables after request",
+ title: 'WORK tables',
+ description: 'Should get WORK tables after request',
test: async () => {
- return adapter.request("common/sendArr", data);
+ return adapter.request('common/sendArr', data)
},
assertion: () => {
- const requests = adapter.getSasRequests();
+ const requests = adapter.getSasRequests()
if (adapter.getSasjsConfig().debug) {
- return requests[0].SASWORK !== null;
+ return requests[0].SASWORK !== null
} else {
- return requests[0].SASWORK === null;
+ return requests[0].SASWORK === null
}
}
},
{
- title: "Make error and capture log",
+ title: 'Make error and capture log',
description:
- "Should make an error and capture log, in the same time it is testing if debug override is working",
+ 'Should make an error and capture log, in the same time it is testing if debug override is working',
test: async () => {
return adapter
- .request("common/makeErr", data, { debug: true })
+ .request('common/makeErr', data, { debug: true })
.catch(() => {
- const sasRequests = adapter.getSasRequests();
+ const sasRequests = adapter.getSasRequests()
const makeErrRequest: any =
- sasRequests.find((req) => req.serviceLink.includes("makeErr")) ||
- null;
+ sasRequests.find((req) => req.serviceLink.includes('makeErr')) ||
+ null
- if (!makeErrRequest) return false;
+ if (!makeErrRequest) return false
return !!(
makeErrRequest.logFile && makeErrRequest.logFile.length > 0
- );
- });
+ )
+ })
},
assertion: (response) => {
- return response;
+ return response
}
}
]
-});
+})
diff --git a/sasjs-tests/src/testSuites/SpecialCases.ts b/sasjs-tests/src/testSuites/SpecialCases.ts
index 4121044..1ec227a 100644
--- a/sasjs-tests/src/testSuites/SpecialCases.ts
+++ b/sasjs-tests/src/testSuites/SpecialCases.ts
@@ -1,91 +1,92 @@
-import SASjs from "@sasjs/adapter";
-import { TestSuite } from "@sasjs/test-framework";
+import SASjs from '@sasjs/adapter'
+import { TestSuite } from '@sasjs/test-framework'
const specialCharData: any = {
table1: [
{
- tab: "\t",
- lf: "\n",
- cr: "\r",
- semicolon: ";semi",
- percent: "%",
+ tab: '\t',
+ lf: '\n',
+ cr: '\r',
+ semicolon: ';semi',
+ percent: '%',
singleQuote: "'",
doubleQuote: '"',
- crlf: "\r\n",
- euro: "€euro",
- banghash: "!#banghash"
+ crlf: '\r\n',
+ euro: '€euro',
+ banghash: '!#banghash',
+ dot: '.'
}
]
-};
+}
const moreSpecialCharData: any = {
table1: [
{
speech0: '"speech',
- pct: "%percent",
+ pct: '%percent',
speech: '"speech',
- slash: "\\slash",
- slashWithSpecial: "\\\tslash",
- macvar: "&sysuserid",
- chinese: "传/傳chinese",
- sigma: "Σsigma",
- at: "@at",
- serbian: "Српски",
- dollar: "$"
+ slash: '\\slash',
+ slashWithSpecial: '\\\tslash',
+ macvar: '&sysuserid',
+ chinese: '传/傳chinese',
+ sigma: 'Σsigma',
+ at: '@at',
+ serbian: 'Српски',
+ dollar: '$'
}
]
-};
+}
const getWideData = () => {
- const cols: any = {};
+ const cols: any = {}
for (let i = 1; i <= 10000; i++) {
- cols["col" + i] = "test" + i;
+ cols['col' + i] = 'test' + i
}
const data: any = {
table1: [cols]
- };
+ }
- return data;
-};
+ return data
+}
const getTables = () => {
- const tables: any = {};
+ const tables: any = {}
for (let i = 1; i <= 100; i++) {
- tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }];
+ tables['table' + i] = [{ col1: 'x', col2: 'x', col3: 'x', col4: 'x' }]
}
- return tables;
-};
+ return tables
+}
const getLargeDataset = () => {
- const rows: any = [];
+ const rows: any = []
const colData: string =
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+ 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
for (let i = 1; i <= 10000; i++) {
- rows.push({ col1: colData, col2: colData, col3: colData, col4: colData });
+ rows.push({ col1: colData, col2: colData, col3: colData, col4: colData })
}
const data: any = {
table1: rows
- };
+ }
- return data;
-};
+ return data
+}
const errorAndCsrfData: any = {
- error: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
- _csrf: [{ col1: "q", col2: "w", col3: "e", col4: "r" }]
-};
+ error: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }],
+ _csrf: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }]
+}
export const specialCaseTests = (adapter: SASjs): TestSuite => ({
- name: "Special Cases",
+ name: 'Special Cases',
tests: [
{
- title: "Common special characters",
- description: "Should handle common special characters",
+ title: 'Common special characters',
+ description: 'Should handle common special characters',
test: () => {
- return adapter.request("common/sendArr", specialCharData);
+ return adapter.request('common/sendArr', specialCharData)
},
assertion: (res: any) => {
return (
@@ -96,17 +97,18 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
res.table1[0][4] === specialCharData.table1[0].percent &&
res.table1[0][5] === specialCharData.table1[0].singleQuote &&
res.table1[0][6] === specialCharData.table1[0].doubleQuote &&
- res.table1[0][7] === "\n" &&
+ res.table1[0][7] === '\n' &&
res.table1[0][8] === specialCharData.table1[0].euro &&
- res.table1[0][9] === specialCharData.table1[0].banghash
- );
+ res.table1[0][9] === specialCharData.table1[0].banghash &&
+ res.table1[0][10] === specialCharData.table1[0].dot
+ )
}
},
{
- title: "Other special characters",
- description: "Should handle other special characters",
+ title: 'Other special characters',
+ description: 'Should handle other special characters',
test: () => {
- return adapter.request("common/sendArr", moreSpecialCharData);
+ return adapter.request('common/sendArr', moreSpecialCharData)
},
assertion: (res: any) => {
return (
@@ -121,50 +123,50 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
res.table1[0][8] === moreSpecialCharData.table1[0].at &&
res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
res.table1[0][10] === moreSpecialCharData.table1[0].dollar
- );
+ )
}
},
{
- title: "Wide table with sendArr",
- description: "Should handle data with 10000 columns",
+ title: 'Wide table with sendArr',
+ description: 'Should handle data with 10000 columns',
test: () => {
- return adapter.request("common/sendArr", getWideData());
+ return adapter.request('common/sendArr', getWideData())
},
assertion: (res: any) => {
- const data = getWideData();
- let result = true;
+ const data = getWideData()
+ let result = true
for (let i = 0; i <= 10; i++) {
result =
- result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
+ result && res.table1[0][i] === data.table1[0]['col' + (i + 1)]
}
- return result;
+ return result
}
},
{
- title: "Wide table with sendObj",
- description: "Should handle data with 10000 columns",
+ title: 'Wide table with sendObj',
+ description: 'Should handle data with 10000 columns',
test: () => {
- return adapter.request("common/sendObj", getWideData());
+ return adapter.request('common/sendObj', getWideData())
},
assertion: (res: any) => {
- const data = getWideData();
- let result = true;
+ const data = getWideData()
+ let result = true
for (let i = 0; i <= 10; i++) {
result =
result &&
- res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
+ res.table1[0]['COL' + (i + 1)] === data.table1[0]['col' + (i + 1)]
}
- return result;
+ return result
}
},
{
- title: "Multiple tables",
- description: "Should handle data with 100 tables",
+ title: 'Multiple tables',
+ description: 'Should handle data with 100 tables',
test: () => {
- return adapter.request("common/sendArr", getTables());
+ return adapter.request('common/sendArr', getTables())
},
assertion: (res: any) => {
- const data = getTables();
+ const data = getTables()
return (
res.table1[0][0] === data.table1[0].col1 &&
res.table1[0][1] === data.table1[0].col2 &&
@@ -174,45 +176,45 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
res.table50[0][1] === data.table50[0].col2 &&
res.table50[0][2] === data.table50[0].col3 &&
res.table50[0][3] === data.table50[0].col4
- );
+ )
}
},
{
- title: "Large dataset with sendObj",
- description: "Should handle 5mb of data",
+ title: 'Large dataset with sendObj',
+ description: 'Should handle 5mb of data',
test: () => {
- return adapter.request("common/sendObj", getLargeDataset());
+ return adapter.request('common/sendObj', getLargeDataset())
},
assertion: (res: any) => {
- const data = getLargeDataset();
- let result = true;
+ const data = getLargeDataset()
+ let result = true
for (let i = 0; i <= 10; i++) {
- result = result && res.table1[i][0] === data.table1[i][0];
+ result = result && res.table1[i][0] === data.table1[i][0]
}
- return result;
+ return result
}
},
{
- title: "Large dataset with sendArr",
- description: "Should handle 5mb of data",
+ title: 'Large dataset with sendArr',
+ description: 'Should handle 5mb of data',
test: () => {
- return adapter.request("common/sendArr", getLargeDataset());
+ return adapter.request('common/sendArr', getLargeDataset())
},
assertion: (res: any) => {
- const data = getLargeDataset();
- let result = true;
+ const data = getLargeDataset()
+ let result = true
for (let i = 0; i <= 10; i++) {
result =
- result && res.table1[i][0] === Object.values(data.table1[i])[0];
+ result && res.table1[i][0] === Object.values(data.table1[i])[0]
}
- return result;
+ return result
}
},
{
- title: "Error and _csrf tables with sendArr",
- description: "Should handle error and _csrf tables",
+ title: 'Error and _csrf tables with sendArr',
+ description: 'Should handle error and _csrf tables',
test: () => {
- return adapter.request("common/sendArr", errorAndCsrfData);
+ return adapter.request('common/sendArr', errorAndCsrfData)
},
assertion: (res: any) => {
return (
@@ -224,14 +226,14 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
- );
+ )
}
},
{
- title: "Error and _csrf tables with sendObj",
- description: "Should handle error and _csrf tables",
+ title: 'Error and _csrf tables with sendObj',
+ description: 'Should handle error and _csrf tables',
test: () => {
- return adapter.request("common/sendObj", errorAndCsrfData);
+ return adapter.request('common/sendObj', errorAndCsrfData)
},
assertion: (res: any) => {
return (
@@ -243,8 +245,8 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
- );
+ )
}
}
]
-});
+})
diff --git a/sasjs-tests/src/utils/Assert.ts b/sasjs-tests/src/utils/Assert.ts
index 9a04a7d..ec6850c 100644
--- a/sasjs-tests/src/utils/Assert.ts
+++ b/sasjs-tests/src/utils/Assert.ts
@@ -1,22 +1,22 @@
export const assert = (
expression: boolean | (() => boolean),
- message = "Assertion failed"
+ message = 'Assertion failed'
) => {
- let result;
+ let result
try {
- if (typeof expression === "boolean") {
- result = expression;
+ if (typeof expression === 'boolean') {
+ result = expression
} else {
- result = expression();
+ result = expression()
}
} catch (e) {
- console.error(message);
- throw new Error(message);
+ console.error(message)
+ throw new Error(message)
}
if (!!result) {
- return;
+ return
} else {
- console.error(message);
- throw new Error(message);
+ console.error(message)
+ throw new Error(message)
}
-};
+}
diff --git a/src/ContextManager.ts b/src/ContextManager.ts
index 3c15ff5..eec0dff 100644
--- a/src/ContextManager.ts
+++ b/src/ContextManager.ts
@@ -314,9 +314,7 @@ export class ContextManager {
contextId: string,
accessToken?: string
): Promise {
- const {
- result: context
- } = await this.requestClient
+ const { result: context } = await this.requestClient
.get(
`${this.serverUrl}/compute/contexts/${contextId}`,
accessToken
diff --git a/src/SAS9ApiClient.ts b/src/SAS9ApiClient.ts
index 473ddcf..cd76e91 100644
--- a/src/SAS9ApiClient.ts
+++ b/src/SAS9ApiClient.ts
@@ -1,4 +1,6 @@
-import axios, { AxiosInstance } from 'axios'
+import { generateTimestamp } from '@sasjs/utils/time'
+import * as NodeFormData from 'form-data'
+import { Sas9RequestClient } from './request/Sas9RequestClient'
import { isUrl } from './utils'
/**
@@ -6,11 +8,11 @@ import { isUrl } from './utils'
*
*/
export class SAS9ApiClient {
- private httpClient: AxiosInstance
+ private requestClient: Sas9RequestClient
- constructor(private serverUrl: string) {
+ constructor(private serverUrl: string, private jobsPath: string) {
if (serverUrl) isUrl(serverUrl)
- this.httpClient = axios.create({ baseURL: this.serverUrl })
+ this.requestClient = new Sas9RequestClient(serverUrl, false)
}
/**
@@ -33,27 +35,52 @@ export class SAS9ApiClient {
/**
* Executes code on a SAS9 server.
* @param linesOfCode - an array of code lines to execute.
- * @param serverName - the server to execute the code on.
- * @param repositoryName - the repository to execute the code in.
+ * @param userName - the user name to log into the current SAS server.
+ * @param password - the password to log into the current SAS server.
*/
public async executeScript(
linesOfCode: string[],
- serverName: string,
- repositoryName: string
+ userName: string,
+ password: string
) {
- const requestPayload = linesOfCode.join('\n')
+ await this.requestClient.login(userName, password, this.jobsPath)
- const executeScriptResponse = await this.httpClient.put(
- `/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
- `command=${requestPayload}`,
- {
- headers: {
- Accept: 'application/json'
- },
- responseType: 'text'
- }
+ const formData = generateFileUploadForm(linesOfCode.join('\n'))
+
+ const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
+ const contentType =
+ 'multipart/form-data; boundary=' + formData.getBoundary()
+ const contentLength = formData.getLengthSync()
+
+ const headers = {
+ 'cache-control': 'no-cache',
+ Accept: '*/*',
+ 'Content-Type': contentType,
+ 'Content-Length': contentLength,
+ Connection: 'keep-alive'
+ }
+ const storedProcessUrl = `${this.jobsPath}/?${
+ '_program=' + codeInjectorPath + '&_debug=log'
+ }`
+ const response = await this.requestClient.post(
+ storedProcessUrl,
+ formData,
+ undefined,
+ contentType,
+ headers
)
- return executeScriptResponse.data
+ return response.result as string
}
}
+
+const generateFileUploadForm = (data: any): NodeFormData => {
+ const formData = new NodeFormData()
+ const filename = `sasjs-execute-sas9-${generateTimestamp('')}.sas`
+ formData.append(filename, data, {
+ filename,
+ contentType: 'text/plain'
+ })
+
+ return formData
+}
diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts
index fff248a..a708332 100644
--- a/src/SASViyaApiClient.ts
+++ b/src/SASViyaApiClient.ts
@@ -28,7 +28,7 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { RequestClient } from './request/RequestClient'
-import { SasAuthResponse } from '@sasjs/utils/types'
+import { SasAuthResponse, MacroVar } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error'
/**
@@ -271,6 +271,7 @@ export class SASViyaApiClient {
* @param waitForResult - when set to true, function will return the session
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
+ * @param variables - an object that represents macro variables.
*/
public async executeScript(
jobPath: string,
@@ -282,7 +283,8 @@ export class SASViyaApiClient {
expectWebout = false,
waitForResult = true,
pollOptions?: PollOptions,
- printPid = false
+ printPid = false,
+ variables?: MacroVar
): Promise {
try {
const headers: any = {
@@ -356,6 +358,8 @@ export class SASViyaApiClient {
: jobPath
}
+ if (variables) jobVariables = { ...jobVariables, ...variables }
+
let files: any[] = []
if (data) {
@@ -412,7 +416,22 @@ export class SASViyaApiClient {
etag,
accessToken,
pollOptions
- ).catch((err) => {
+ ).catch(async (err) => {
+ const error = err?.response?.data
+ const result = /err=[0-9]*,/.exec(error)
+
+ const errorCode = '5113'
+ if (result?.[0]?.slice(4, -1) === errorCode) {
+ const sessionLogUrl =
+ postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
+ const logCount = 1000000
+ err.log = await fetchLogByChunks(
+ this.requestClient,
+ accessToken!,
+ sessionLogUrl,
+ logCount
+ )
+ }
throw prefixMessage(err, 'Error while polling job status. ')
})
@@ -579,16 +598,15 @@ export class SASViyaApiClient {
}
}
- const {
- result: createFolderResponse
- } = await this.requestClient.post(
- `/folders/folders?parentFolderUri=${parentFolderUri}`,
- {
- name: folderName,
- type: 'folder'
- },
- accessToken
- )
+ const { result: createFolderResponse } =
+ await this.requestClient.post(
+ `/folders/folders?parentFolderUri=${parentFolderUri}`,
+ {
+ name: folderName,
+ type: 'folder'
+ },
+ accessToken
+ )
// update folder map with newly created folder.
await this.populateFolderMap(
@@ -705,13 +723,11 @@ export class SASViyaApiClient {
let formData
if (typeof FormData === 'undefined') {
formData = new NodeFormData()
- formData.append('grant_type', 'authorization_code')
- formData.append('code', authCode)
} else {
formData = new FormData()
- formData.append('grant_type', 'authorization_code')
- formData.append('code', authCode)
}
+ formData.append('grant_type', 'authorization_code')
+ formData.append('code', authCode)
const authResponse = await this.requestClient
.post(
@@ -800,6 +816,7 @@ export class SASViyaApiClient {
* @param expectWebout - a boolean indicating whether to expect a _webout response.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
+ * @param variables - an object that represents macro variables.
*/
public async executeComputeJob(
sasJob: string,
@@ -810,7 +827,8 @@ export class SASViyaApiClient {
waitForResult = true,
expectWebout = false,
pollOptions?: PollOptions,
- printPid = false
+ printPid = false,
+ variables?: MacroVar
) {
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
@@ -860,9 +878,7 @@ export class SASViyaApiClient {
throw new Error(`URI of job definition was not found.`)
}
- const {
- result: jobDefinition
- } = await this.requestClient
+ const { result: jobDefinition } = await this.requestClient
.get(
`${this.serverUrl}${jobDefinitionLink.href}`,
accessToken
@@ -891,7 +907,8 @@ export class SASViyaApiClient {
expectWebout,
waitForResult,
pollOptions,
- printPid
+ printPid,
+ variables
)
}
@@ -1066,6 +1083,7 @@ export class SASViyaApiClient {
) {
let POLL_INTERVAL = 300
let MAX_POLL_COUNT = 1000
+ let MAX_ERROR_COUNT = 5
if (pollOptions) {
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
@@ -1074,6 +1092,7 @@ export class SASViyaApiClient {
let postedJobState = ''
let pollCount = 0
+ let errorCount = 0
const headers: any = {
'Content-Type': 'application/json',
'If-None-Match': etag
@@ -1088,14 +1107,18 @@ export class SASViyaApiClient {
const { result: state } = await this.requestClient
.get(
- `${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
+ `${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
accessToken,
'text/plain',
{},
this.debug
)
.catch((err) => {
- throw prefixMessage(err, 'Error while getting job state. ')
+ console.error(
+ `Error fetching job state from ${this.serverUrl}${stateLink.href}. Starting poll, assuming job to be running.`,
+ err
+ )
+ return { result: 'unavailable' }
})
const currentState = state.trim()
@@ -1110,25 +1133,40 @@ export class SASViyaApiClient {
if (
postedJobState === 'running' ||
postedJobState === '' ||
- postedJobState === 'pending'
+ postedJobState === 'pending' ||
+ postedJobState === 'unavailable'
) {
if (stateLink) {
const { result: jobState } = await this.requestClient
.get(
- `${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
+ `${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
accessToken,
'text/plain',
{},
this.debug
)
.catch((err) => {
- throw prefixMessage(
- err,
- 'Error while getting job state after interval. '
+ errorCount++
+ if (
+ pollCount >= MAX_POLL_COUNT ||
+ errorCount >= MAX_ERROR_COUNT
+ ) {
+ throw prefixMessage(
+ err,
+ 'Error while getting job state after interval. '
+ )
+ }
+ console.error(
+ `Error fetching job state from ${this.serverUrl}${stateLink.href}. Resuming poll, assuming job to be running.`,
+ err
)
+ return { result: 'unavailable' }
})
postedJobState = jobState.trim()
+ if (postedJobState != 'unavailable' && errorCount > 0) {
+ errorCount = 0
+ }
if (this.debug && printedState !== postedJobState) {
console.log('Polling job status...')
diff --git a/src/SASjs.ts b/src/SASjs.ts
index 574e374..e88f78f 100644
--- a/src/SASjs.ts
+++ b/src/SASjs.ts
@@ -4,13 +4,14 @@ import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader'
import { AuthManager } from './auth'
-import { ServerType } from '@sasjs/utils/types'
+import { ServerType, MacroVar } from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient'
import {
JobExecutor,
WebJobExecutor,
ComputeJobExecutor,
- JesJobExecutor
+ JesJobExecutor,
+ Sas9JobExecutor
} from './job-execution'
import { ErrorResponse } from './types/errors'
@@ -41,6 +42,7 @@ export default class SASjs {
private webJobExecutor: JobExecutor | null = null
private computeJobExecutor: JobExecutor | null = null
private jesJobExecutor: JobExecutor | null = null
+ private sas9JobExecutor: JobExecutor | null = null
constructor(config?: any) {
this.sasjsConfig = {
@@ -57,15 +59,15 @@ export default class SASjs {
public async executeScriptSAS9(
linesOfCode: string[],
- serverName: string,
- repositoryName: string
+ userName: string,
+ password: string
) {
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
return await this.sas9ApiClient?.executeScript(
linesOfCode,
- serverName,
- repositoryName
+ userName,
+ password
)
}
@@ -569,6 +571,12 @@ export default class SASjs {
accessToken
)
}
+ } else if (
+ config.serverType === ServerType.Sas9 &&
+ config.username &&
+ config.password
+ ) {
+ return await this.sas9JobExecutor!.execute(sasJob, data, config)
} else {
return await this.webJobExecutor!.execute(
sasJob,
@@ -616,7 +624,7 @@ export default class SASjs {
)
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
- sasApiClient = new SAS9ApiClient(serverUrl)
+ sasApiClient = new SAS9ApiClient(serverUrl, this.jobsPath)
}
} else {
let sasClientConfig: any = null
@@ -663,6 +671,7 @@ export default class SASjs {
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
+ * @param variables - an object that represents macro variables.
*/
public async startComputeJob(
sasJob: string,
@@ -671,7 +680,8 @@ export default class SASjs {
accessToken?: string,
waitForResult?: boolean,
pollOptions?: PollOptions,
- printPid = false
+ printPid = false,
+ variables?: MacroVar
) {
config = {
...this.sasjsConfig,
@@ -694,7 +704,8 @@ export default class SASjs {
!!waitForResult,
false,
pollOptions,
- printPid
+ printPid,
+ variables
)
}
@@ -805,7 +816,11 @@ export default class SASjs {
if (this.sasjsConfig.serverType === ServerType.Sas9) {
if (this.sas9ApiClient)
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
- else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl)
+ else
+ this.sas9ApiClient = new SAS9ApiClient(
+ this.sasjsConfig.serverUrl,
+ this.jobsPath
+ )
}
this.fileUploader = new FileUploader(
@@ -823,6 +838,12 @@ export default class SASjs {
this.sasViyaApiClient!
)
+ this.sas9JobExecutor = new Sas9JobExecutor(
+ this.sasjsConfig.serverUrl,
+ this.sasjsConfig.serverType!,
+ this.jobsPath
+ )
+
this.computeJobExecutor = new ComputeJobExecutor(
this.sasjsConfig.serverUrl,
this.sasViyaApiClient!
diff --git a/src/SessionManager.ts b/src/SessionManager.ts
index 3c780dd..2120471 100644
--- a/src/SessionManager.ts
+++ b/src/SessionManager.ts
@@ -91,10 +91,7 @@ export class SessionManager {
}
private async createAndWaitForSession(accessToken?: string) {
- const {
- result: createdSession,
- etag
- } = await this.requestClient
+ const { result: createdSession, etag } = await this.requestClient
.post(
`${this.serverUrl}/compute/contexts/${
this.currentContext!.id
diff --git a/src/auth/spec/AuthManager.spec.ts b/src/auth/spec/AuthManager.spec.ts
index b528912..584dc04 100644
--- a/src/auth/spec/AuthManager.spec.ts
+++ b/src/auth/spec/AuthManager.spec.ts
@@ -57,7 +57,7 @@ describe('AuthManager', () => {
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
})
- it('should call the auth callback and return when already logged in', async (done) => {
+ it('should call the auth callback and return when already logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -77,10 +77,9 @@ describe('AuthManager', () => {
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) => {
+ it('should post a login request to the server if not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -121,10 +120,9 @@ describe('AuthManager', () => {
}
)
expect(authCallback).toHaveBeenCalledTimes(1)
- done()
})
- it('should parse and submit the authorisation form when necessary', async (done) => {
+ it('should parse and submit the authorisation form when necessary', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -160,10 +158,9 @@ describe('AuthManager', () => {
expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse
)
- done()
})
- it('should check and return session information if logged in', async (done) => {
+ it('should check and return session information if logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -189,7 +186,5 @@ describe('AuthManager', () => {
}
}
)
-
- done()
})
})
diff --git a/src/job-execution/JesJobExecutor.ts b/src/job-execution/JesJobExecutor.ts
index 60cf52f..42d22a4 100644
--- a/src/job-execution/JesJobExecutor.ts
+++ b/src/job-execution/JesJobExecutor.ts
@@ -33,7 +33,7 @@ export class JesJobExecutor extends BaseJobExecutor {
.then((response) => {
this.appendRequest(response, sasJob, config.debug)
- resolve(response.result)
+ resolve(response)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
diff --git a/src/job-execution/Sas9JobExecutor.ts b/src/job-execution/Sas9JobExecutor.ts
new file mode 100644
index 0000000..6dbed70
--- /dev/null
+++ b/src/job-execution/Sas9JobExecutor.ts
@@ -0,0 +1,110 @@
+import { ServerType } from '@sasjs/utils/types'
+import * as NodeFormData from 'form-data'
+import { ErrorResponse } from '../types/errors'
+import { convertToCSV, isRelativePath } from '../utils'
+import { BaseJobExecutor } from './JobExecutor'
+import { Sas9RequestClient } from '../request/Sas9RequestClient'
+
+/**
+ * Job executor for SAS9 servers for use in Node.js environments.
+ * Initiates login with the provided username and password from the config
+ * The cookies are stored in the request client and used in subsequent
+ * job execution requests.
+ */
+export class Sas9JobExecutor extends BaseJobExecutor {
+ private requestClient: Sas9RequestClient
+ constructor(
+ serverUrl: string,
+ serverType: ServerType,
+ private jobsPath: string
+ ) {
+ super(serverUrl, serverType)
+ this.requestClient = new Sas9RequestClient(serverUrl, false)
+ }
+
+ async execute(sasJob: string, data: any, config: any) {
+ const program = isRelativePath(sasJob)
+ ? config.appLoc
+ ? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
+ : sasJob
+ : sasJob
+ let apiUrl = `${config.serverUrl}${this.jobsPath}?${'_program=' + program}`
+ apiUrl = `${apiUrl}${
+ config.username && config.password
+ ? '&_username=' + config.username + '&_password=' + config.password
+ : ''
+ }`
+
+ let requestParams = {
+ ...this.getRequestParams(config)
+ }
+
+ let formData = new NodeFormData()
+
+ if (data) {
+ try {
+ formData = generateFileUploadForm(formData, data)
+ } catch (e) {
+ return Promise.reject(new ErrorResponse(e?.message, e))
+ }
+ }
+
+ for (const key in requestParams) {
+ if (requestParams.hasOwnProperty(key)) {
+ formData.append(key, requestParams[key])
+ }
+ }
+
+ await this.requestClient.login(
+ config.username,
+ config.password,
+ this.jobsPath
+ )
+ const contentType =
+ data && Object.keys(data).length
+ ? 'multipart/form-data; boundary=' + (formData as any)._boundary
+ : 'text/plain'
+ return await this.requestClient!.post(
+ apiUrl,
+ formData,
+ undefined,
+ contentType,
+ {
+ Accept: '*/*',
+ Connection: 'Keep-Alive'
+ }
+ )
+ }
+
+ private getRequestParams(config: any): any {
+ const requestParams: any = {}
+
+ if (config.debug) {
+ requestParams['_debug'] = 131
+ }
+
+ return requestParams
+ }
+}
+
+const generateFileUploadForm = (
+ formData: NodeFormData,
+ data: any
+): NodeFormData => {
+ 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.'
+ )
+ }
+
+ formData.append(name, csv, {
+ filename: `${name}.csv`,
+ contentType: 'application/csv'
+ })
+ }
+
+ return formData
+}
diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts
index 816c6d7..61f215c 100644
--- a/src/job-execution/WebJobExecutor.ts
+++ b/src/job-execution/WebJobExecutor.ts
@@ -71,10 +71,8 @@ export class WebJobExecutor extends BaseJobExecutor {
} else {
// param based approach
try {
- const {
- formData: newFormData,
- requestParams: params
- } = generateTableUploadForm(formData, data)
+ const { formData: newFormData, requestParams: params } =
+ generateTableUploadForm(formData, data)
formData = newFormData
requestParams = { ...requestParams, ...params }
} catch (e) {
diff --git a/src/job-execution/index.ts b/src/job-execution/index.ts
index d71b564..dc9187c 100644
--- a/src/job-execution/index.ts
+++ b/src/job-execution/index.ts
@@ -1,4 +1,5 @@
export * from './ComputeJobExecutor'
export * from './JesJobExecutor'
export * from './JobExecutor'
+export * from './Sas9JobExecutor'
export * from './WebJobExecutor'
diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts
index c55060b..62a144e 100644
--- a/src/request/RequestClient.ts
+++ b/src/request/RequestClient.ts
@@ -10,6 +10,7 @@ import {
} from '../types/errors'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error'
+import { SAS9AuthError } from '../types/errors/SAS9AuthError'
export interface HttpClient {
get(
@@ -44,11 +45,11 @@ export interface HttpClient {
}
export class RequestClient implements HttpClient {
- private csrfToken: CsrfToken = { headerName: '', value: '' }
- private fileUploadCsrfToken: CsrfToken | undefined
- private httpClient: AxiosInstance
+ protected csrfToken: CsrfToken = { headerName: '', value: '' }
+ protected fileUploadCsrfToken: CsrfToken | undefined
+ protected httpClient: AxiosInstance
- constructor(private baseUrl: string, allowInsecure = false) {
+ constructor(protected baseUrl: string, allowInsecure = false) {
const https = require('https')
if (allowInsecure && https.Agent) {
this.httpClient = axios.create({
@@ -214,9 +215,8 @@ export class RequestClient implements HttpClient {
const headers = this.getHeaders(accessToken, 'application/json')
if (this.fileUploadCsrfToken?.value) {
- headers[
- this.fileUploadCsrfToken.headerName
- ] = this.fileUploadCsrfToken.value
+ headers[this.fileUploadCsrfToken.headerName] =
+ this.fileUploadCsrfToken.value
}
try {
@@ -291,7 +291,7 @@ export class RequestClient implements HttpClient {
})
}
- private getHeaders = (
+ protected getHeaders = (
accessToken: string | undefined,
contentType: string
) => {
@@ -316,7 +316,7 @@ export class RequestClient implements HttpClient {
return headers
}
- private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
+ protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -324,7 +324,7 @@ export class RequestClient implements HttpClient {
}
}
- private parseAndSetCsrfToken = (response: AxiosResponse) => {
+ protected parseAndSetCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -333,9 +333,9 @@ export class RequestClient implements HttpClient {
}
private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => {
- const tokenHeader = (response.headers[
- 'x-csrf-header'
- ] as string)?.toLowerCase()
+ const tokenHeader = (
+ response.headers['x-csrf-header'] as string
+ )?.toLowerCase()
if (tokenHeader) {
const token = response.headers[tokenHeader]
@@ -348,7 +348,7 @@ export class RequestClient implements HttpClient {
}
}
- private handleError = async (
+ protected handleError = async (
e: any,
callback: any,
debug: boolean = false
@@ -406,7 +406,7 @@ export class RequestClient implements HttpClient {
throw e
}
- private parseResponse(response: AxiosResponse) {
+ protected parseResponse(response: AxiosResponse) {
const etag = response?.headers ? response.headers['etag'] : ''
let parsedResponse
let includeSAS9Log: boolean = false
@@ -440,7 +440,7 @@ export class RequestClient implements HttpClient {
}
}
-const throwIfError = (response: AxiosResponse) => {
+export const throwIfError = (response: AxiosResponse) => {
if (response.status === 401) {
throw new LoginRequiredError()
}
@@ -471,6 +471,10 @@ const throwIfError = (response: AxiosResponse) => {
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
}
+ if (response.config?.url?.includes('sasAuthError')) {
+ throw new SAS9AuthError()
+ }
+
const error = parseError(response.data as string)
if (error) {
diff --git a/src/request/Sas9RequestClient.ts b/src/request/Sas9RequestClient.ts
new file mode 100644
index 0000000..eedb3ef
--- /dev/null
+++ b/src/request/Sas9RequestClient.ts
@@ -0,0 +1,121 @@
+import { AxiosRequestConfig } from 'axios'
+import axiosCookieJarSupport from 'axios-cookiejar-support'
+import * as tough from 'tough-cookie'
+import { prefixMessage } from '@sasjs/utils/error'
+import { RequestClient, throwIfError } from './RequestClient'
+
+/**
+ * Specific request client for SAS9 in Node.js environments.
+ * Handles redirects and cookie management.
+ */
+export class Sas9RequestClient extends RequestClient {
+ constructor(baseUrl: string, allowInsecure = false) {
+ super(baseUrl, allowInsecure)
+ this.httpClient.defaults.maxRedirects = 0
+ this.httpClient.defaults.validateStatus = (status) =>
+ status >= 200 && status < 303
+
+ if (axiosCookieJarSupport) {
+ axiosCookieJarSupport(this.httpClient)
+ this.httpClient.defaults.jar = new tough.CookieJar()
+ }
+ }
+
+ public async login(username: string, password: string, jobsPath: string) {
+ const codeInjectorPath = `/User Folders/${username}/My Folder/sasjs/runner`
+ if (this.httpClient.defaults.jar) {
+ ;(this.httpClient.defaults.jar as tough.CookieJar).removeAllCookies()
+ await this.get(
+ `${jobsPath}?_program=${codeInjectorPath}&_username=${username}&_password=${password}`,
+ undefined,
+ 'text/plain'
+ )
+ }
+ }
+
+ public async get(
+ url: string,
+ accessToken: string | undefined,
+ contentType: string = 'application/json',
+ overrideHeaders: { [key: string]: string | number } = {},
+ debug: boolean = false
+ ): 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(url, requestConfig)
+ .then((response) => {
+ if (response.status === 302) {
+ return this.get(
+ response.headers['location'],
+ accessToken,
+ contentType
+ )
+ }
+ throwIfError(response)
+ return this.parseResponse(response)
+ })
+ .catch(async (e) => {
+ return await this.handleError(
+ e,
+ () =>
+ this.get(url, accessToken, contentType, overrideHeaders).catch(
+ (err) => {
+ throw prefixMessage(
+ err,
+ 'Error while executing handle error callback. '
+ )
+ }
+ ),
+ debug
+ ).catch((err) => {
+ throw prefixMessage(err, 'Error while handling error. ')
+ })
+ })
+ }
+
+ public post(
+ 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(url, data, { headers, withCredentials: true })
+ .then(async (response) => {
+ if (response.status === 302) {
+ return await this.get(
+ response.headers['location'],
+ undefined,
+ contentType,
+ overrideHeaders
+ )
+ }
+ throwIfError(response)
+ return this.parseResponse(response)
+ })
+ .catch(async (e) => {
+ return await this.handleError(e, () =>
+ this.post(url, data, accessToken, contentType, overrideHeaders)
+ )
+ })
+ }
+}
diff --git a/src/test/FileUploader.spec.ts b/src/test/FileUploader.spec.ts
index 9e225a2..4b6f678 100644
--- a/src/test/FileUploader.spec.ts
+++ b/src/test/FileUploader.spec.ts
@@ -1,3 +1,7 @@
+/**
+ * @jest-environment jsdom
+ */
+
import { FileUploader } from '../FileUploader'
import { UploadFile } from '../types'
import { RequestClient } from '../request/RequestClient'
@@ -35,41 +39,40 @@ describe('FileUploader', () => {
new RequestClient('https://sample.server.com')
)
- it('should upload successfully', async (done) => {
+ it('should upload successfully', async () => {
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()
- })
+ const res = await fileUploader.uploadFile(sasJob, files, params)
+
+ expect(res).toEqual(JSON.parse(sampleResponse))
})
- it('should an error when no files are provided', async (done) => {
+ it('should an error when no files are provided', async () => {
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()
- })
+ const err = await fileUploader
+ .uploadFile(sasJob, files, params)
+ .catch((err: any) => err)
+ expect(err.error.message).toEqual('At least one file must be provided.')
})
- it('should throw an error when no sasJob is provided', async (done) => {
+ it('should throw an error when no sasJob is provided', async () => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
- fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
- expect(err.error.message).toEqual('sasJob must be provided.')
- done()
- })
+ const err = await fileUploader
+ .uploadFile(sasJob, files, params)
+ .catch((err: any) => err)
+ expect(err.error.message).toEqual('sasJob must be provided.')
})
- it('should throw an error when login is required', async (done) => {
+ it('should throw an error when login is required', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '