([])
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 8ebd3dc..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, undefined, () => {
- 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/SASViyaApiClient.ts b/src/SASViyaApiClient.ts
index d688807..72e9fa6 100644
--- a/src/SASViyaApiClient.ts
+++ b/src/SASViyaApiClient.ts
@@ -412,7 +412,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. ')
})
@@ -1063,6 +1078,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
@@ -1071,6 +1087,7 @@ export class SASViyaApiClient {
let postedJobState = ''
let pollCount = 0
+ let errorCount = 0
const headers: any = {
'Content-Type': 'application/json',
'If-None-Match': etag
@@ -1085,14 +1102,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()
@@ -1107,25 +1128,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..4d9c71c 100644
--- a/src/SASjs.ts
+++ b/src/SASjs.ts
@@ -10,7 +10,8 @@ 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 = {
@@ -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,
@@ -823,6 +831,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/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/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 c38d506..991b7a3 100644
--- a/src/request/RequestClient.ts
+++ b/src/request/RequestClient.ts
@@ -44,11 +44,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({
@@ -290,7 +290,7 @@ export class RequestClient implements HttpClient {
})
}
- private getHeaders = (
+ protected getHeaders = (
accessToken: string | undefined,
contentType: string
) => {
@@ -315,7 +315,7 @@ export class RequestClient implements HttpClient {
return headers
}
- private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
+ protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -323,7 +323,7 @@ export class RequestClient implements HttpClient {
}
}
- private parseAndSetCsrfToken = (response: AxiosResponse) => {
+ protected parseAndSetCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -347,7 +347,7 @@ export class RequestClient implements HttpClient {
}
}
- private handleError = async (
+ protected handleError = async (
e: any,
callback: any,
debug: boolean = false
@@ -405,7 +405,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
@@ -439,7 +439,7 @@ export class RequestClient implements HttpClient {
}
}
-const throwIfError = (response: AxiosResponse) => {
+export const throwIfError = (response: AxiosResponse) => {
if (response.status === 401) {
throw new LoginRequiredError()
}
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/utils/isUrl.spec.ts b/src/test/utils/isUrl.spec.ts
new file mode 100644
index 0000000..928048c
--- /dev/null
+++ b/src/test/utils/isUrl.spec.ts
@@ -0,0 +1,39 @@
+import { isUrl } from '../../utils/isUrl'
+
+describe('urlValidator', () => {
+ it('should return true with an HTTP URL', () => {
+ const url = 'http://google.com'
+
+ expect(isUrl(url)).toEqual(true)
+ })
+
+ it('should return true with an HTTPS URL', () => {
+ const url = 'https://google.com'
+
+ expect(isUrl(url)).toEqual(true)
+ })
+
+ it('should return true when the URL is blank', () => {
+ const url = ''
+
+ expect(isUrl(url)).toEqual(false)
+ })
+
+ it('should return false when the URL has not supported protocol', () => {
+ const url = 'htpps://google.com'
+
+ expect(isUrl(url)).toEqual(false)
+ })
+
+ it('should return false when the URL is null', () => {
+ const url = null
+
+ expect(isUrl(url as unknown as string)).toEqual(false)
+ })
+
+ it('should return false when the URL is undefined', () => {
+ const url = undefined
+
+ expect(isUrl(url as unknown as string)).toEqual(false)
+ })
+})
diff --git a/src/utils/convertToCsv.spec.ts b/src/utils/convertToCsv.spec.ts
new file mode 100644
index 0000000..8a56335
--- /dev/null
+++ b/src/utils/convertToCsv.spec.ts
@@ -0,0 +1,170 @@
+import { convertToCSV } from './convertToCsv'
+
+describe('convertToCsv', () => {
+ it('should convert single quoted values', () => {
+ const data = [
+ { foo: `'bar'`, bar: 'abc' },
+ { foo: 'sadf', bar: 'def' },
+ { foo: 'asd', bar: `'qwert'` }
+ ]
+
+ const expectedOutput = `foo:$char5. bar:$char7.\r\n"'bar'",abc\r\nsadf,def\r\nasd,"'qwert'"`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert double quoted values', () => {
+ const data = [
+ { foo: `"bar"`, bar: 'abc' },
+ { foo: 'sadf', bar: 'def' },
+ { foo: 'asd', bar: `"qwert"` }
+ ]
+
+ const expectedOutput = `foo:$char5. bar:$char7.\r\n"""bar""",abc\r\nsadf,def\r\nasd,"""qwert"""`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with mixed quotes', () => {
+ const data = [{ foo: `'blah'`, bar: `"blah"` }]
+
+ const expectedOutput = `foo:$char6. bar:$char6.\r\n"'blah'","""blah"""`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with mixed quotes', () => {
+ const data = [{ foo: `'blah,"'`, bar: `"blah,blah" "` }]
+
+ const expectedOutput = `foo:$char8. bar:$char13.\r\n"'blah,""'","""blah,blah"" """`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with mixed quotes', () => {
+ const data = [{ foo: `',''`, bar: `","` }]
+
+ const expectedOutput = `foo:$char4. bar:$char3.\r\n"',''",""","""`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with mixed quotes', () => {
+ const data = [{ foo: `','`, bar: `,"` }]
+
+ const expectedOutput = `foo:$char3. bar:$char2.\r\n"','",","""`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with mixed quotes', () => {
+ const data = [{ foo: `"`, bar: `'` }]
+
+ const expectedOutput = `foo:$char1. bar:$char1.\r\n"""","'"`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with mixed quotes', () => {
+ const data = [{ foo: `,`, bar: `',` }]
+
+ const expectedOutput = `foo:$char1. bar:$char2.\r\n",","',"`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with number cases 1', () => {
+ const data = [
+ { 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 expectedOutput = `col1:best. col2:best. col3:$char1. col4:$char1.\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with number cases 2', () => {
+ const data = [
+ { 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 expectedOutput = `col1:best. col2:best. col3:$char1. col4:$char1.\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,\r\n42,1.62,x,x\r\n42,1.62,x,x`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+ })
+
+ it('should convert values with common special characters', () => {
+ expect(convertToCSV([{ tab: '\t' }])).toEqual(`tab:$char1.\r\n\"\t\"`)
+ expect(convertToCSV([{ lf: '\n' }])).toEqual(`lf:$char1.\r\n\"\n\"`)
+ expect(convertToCSV([{ semicolon: ';semi' }])).toEqual(
+ `semicolon:$char5.\r\n;semi`
+ )
+ expect(convertToCSV([{ percent: '%' }])).toEqual(`percent:$char1.\r\n%`)
+ expect(convertToCSV([{ singleQuote: "'" }])).toEqual(
+ `singleQuote:$char1.\r\n\"'\"`
+ )
+ expect(convertToCSV([{ doubleQuote: '"' }])).toEqual(
+ `doubleQuote:$char1.\r\n""""`
+ )
+ expect(convertToCSV([{ crlf: '\r\n' }])).toEqual(`crlf:$char2.\r\n\"\n\"`)
+ expect(convertToCSV([{ euro: '€euro' }])).toEqual(`euro:$char7.\r\n€euro`)
+ expect(convertToCSV([{ banghash: '!#banghash' }])).toEqual(
+ `banghash:$char10.\r\n!#banghash`
+ )
+ })
+
+ it('should convert values with other special characters', () => {
+ const data = [
+ {
+ speech0: '"speech',
+ pct: '%percent',
+ speech: '"speech',
+ slash: '\\slash',
+ slashWithSpecial: '\\\tslash',
+ macvar: '&sysuserid',
+ chinese: '传/傳chinese',
+ sigma: 'Σsigma',
+ at: '@at',
+ serbian: 'Српски',
+ dollar: '$'
+ }
+ ]
+
+ const expectedOutput = `speech0:$char7. pct:$char8. speech:$char7. slash:$char6. slashWithSpecial:$char7. macvar:$char10. chinese:$char14. sigma:$char7. at:$char3. serbian:$char12. dollar:$char1.\r\n"""speech",%percent,"""speech",\\slash,\"\\\tslash\",&sysuserid,传/傳chinese,Σsigma,@at,Српски,$`
+
+ expect(convertToCSV(data)).toEqual(expectedOutput)
+
+ expect(convertToCSV([{ speech: 'menext' }])).toEqual(
+ `speech:$char6.\r\nmenext`
+ )
+ expect(convertToCSV([{ speech: 'me\nnext' }])).toEqual(
+ `speech:$char7.\r\n\"me\nnext\"`
+ )
+ expect(convertToCSV([{ speech: `me'next` }])).toEqual(
+ `speech:$char7.\r\n\"me'next\"`
+ )
+ expect(convertToCSV([{ speech: `me"next` }])).toEqual(
+ `speech:$char7.\r\n\"me""next\"`
+ )
+ expect(convertToCSV([{ speech: `me""next` }])).toEqual(
+ `speech:$char8.\r\n\"me""""next\"`
+ )
+ expect(convertToCSV([{ slashWithSpecial: '\\\tslash' }])).toEqual(
+ `slashWithSpecial:$char7.\r\n\"\\\tslash\"`
+ )
+ expect(convertToCSV([{ slashWithSpecial: '\\ \tslash' }])).toEqual(
+ `slashWithSpecial:$char8.\r\n\"\\ \tslash\"`
+ )
+ expect(
+ convertToCSV([{ slashWithSpecialExtra: '\\\ts\tl\ta\ts\t\th\t' }])
+ ).toEqual(`slashWithSpecialExtra:$char13.\r\n\"\\\ts\tl\ta\ts\t\th\t\"`)
+ })
+})
diff --git a/src/utils/convertToCsv.ts b/src/utils/convertToCsv.ts
index 08a33b1..9496514 100644
--- a/src/utils/convertToCsv.ts
+++ b/src/utils/convertToCsv.ts
@@ -1,6 +1,6 @@
/**
- * Converts the given JSON object to a CSV string.
- * @param data - the JSON object to convert.
+ * Converts the given JSON object array to a CSV string.
+ * @param data - the array of JSON objects to convert.
*/
export const convertToCSV = (data: any) => {
const replacer = (key: any, value: any) => (value === null ? '' : value)
@@ -37,15 +37,7 @@ export const convertToCSV = (data: any) => {
let byteSize
if (typeof row[field] === 'string') {
- let doubleQuotesFound = row[field]
- .split('')
- .filter((char: any) => char === '"')
-
byteSize = getByteSize(row[field])
-
- if (doubleQuotesFound.length > 0) {
- byteSize += doubleQuotesFound.length
- }
}
return byteSize
@@ -61,7 +53,7 @@ export const convertToCSV = (data: any) => {
)
}
- return `${field}:${firstFoundType === 'chars' ? '$' : ''}${
+ return `${field}:${firstFoundType === 'chars' ? '$char' : ''}${
longestValueForField
? longestValueForField
: firstFoundType === 'chars'
@@ -73,35 +65,28 @@ export const convertToCSV = (data: any) => {
if (invalidString) {
return 'ERROR: LARGE STRING LENGTH'
}
+
csvTest = data.map((row: any) => {
const fields = Object.keys(row).map((fieldName, index) => {
let value
- let containsSpecialChar = false
const currentCell = row[fieldName]
- if (JSON.stringify(currentCell).search(/(\\t|\\n|\\r)/gm) > -1) {
- value = currentCell.toString()
- containsSpecialChar = true
- } else {
- value = JSON.stringify(currentCell, replacer)
- }
+ if (typeof currentCell === 'number') return currentCell
- value = value.replace(/\\\\/gm, '\\')
+ // stringify with replacer converts null values to empty strings
+ value = currentCell === null ? '' : currentCell
- if (containsSpecialChar) {
- if (value.includes(',') || value.includes('"')) {
- value = '"' + value + '"'
- }
- } else {
- if (
- !value.includes(',') &&
- value.includes('"') &&
- !value.includes('\\"')
- ) {
- value = value.substring(1, value.length - 1)
- }
+ // if there any present, it should have preceding (") for escaping
+ value = value.replace(/"/g, `""`)
- value = value.replace(/\\"/gm, '""')
+ // also wraps the value in double quotes
+ value = `"${value}"`
+
+ if (
+ value.substring(1, value.length - 1).search(/(\t|\n|\r|,|\'|\")/gm) < 0
+ ) {
+ // Remove wrapping quotes for values that don't contain special characters
+ value = value.substring(1, value.length - 1)
}
value = value.replace(/\r\n/gm, '\n')
diff --git a/src/utils/isUrl.ts b/src/utils/isUrl.ts
index 31d321b..4f38faf 100644
--- a/src/utils/isUrl.ts
+++ b/src/utils/isUrl.ts
@@ -1,16 +1,17 @@
/**
* Checks if string is in URL format.
- * @param url - string to check.
+ * @param str - string to check.
*/
-export const isUrl = (url: string): boolean => {
- const pattern = new RegExp(
- '^(http://|https://)[a-z0-9]+([-.]{1}[a-z0-9]+)*.[a-z]{2,5}(:[0-9]{1,5})?(/.*)?$',
- 'gi'
- )
+export const isUrl = (str: string): boolean => {
+ const supportedProtocols = ['http:', 'https:']
- if (pattern.test(url)) return true
- else
- throw new Error(
- `'${url}' is not a valid url. An example of a valid url is 'http://valid-url.com'.`
- )
+ try {
+ const url = new URL(str)
+
+ if (!supportedProtocols.includes(url.protocol)) return false
+ } catch (_) {
+ return false
+ }
+
+ return true
}