From 0114a80e382c6d41d760360e31fc0951656ea3a2 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Fri, 9 Jul 2021 09:17:49 +0100 Subject: [PATCH] chore(execute): add tests for executeScript --- src/api/viya/spec/executeScript.spec.ts | 678 ++++++++++++++++++++++++ src/api/viya/spec/mockResponses.ts | 56 ++ 2 files changed, 734 insertions(+) create mode 100644 src/api/viya/spec/executeScript.spec.ts create mode 100644 src/api/viya/spec/mockResponses.ts diff --git a/src/api/viya/spec/executeScript.spec.ts b/src/api/viya/spec/executeScript.spec.ts new file mode 100644 index 0000000..03af686 --- /dev/null +++ b/src/api/viya/spec/executeScript.spec.ts @@ -0,0 +1,678 @@ +import { RequestClient } from '../../../request/RequestClient' +import { SessionManager } from '../../../SessionManager' +import { executeScript } from '../executeScript' +import { mockSession, mockAuthConfig, mockJob } from './mockResponses' +import * as pollJobStateModule from '../pollJobState' +import * as uploadTablesModule from '../uploadTables' +import * as tokensModule from '../../../auth/tokens' +import * as formatDataModule from '../../../utils/formatDataForRequest' +import * as fetchLogsModule from '../../../utils/fetchLogByChunks' +import { PollOptions } from '../../../types' +import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors' +import { Logger, LogLevel } from '@sasjs/utils' + +const sessionManager = new (>SessionManager)() +const requestClient = new (>RequestClient)() +const defaultPollOptions: PollOptions = { + maxPollCount: 100, + pollInterval: 500, + streamLog: false +} + +describe('executeScript', () => { + beforeEach(() => { + ;(process as any).logger = new Logger(LogLevel.Off) + setupMocks() + }) + + it('should not try to get fresh tokens if an authConfig is not provided', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context' + ) + + expect(tokensModule.getTokens).not.toHaveBeenCalled() + }) + + it('should try to get fresh tokens if an authConfig is provided', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context', + mockAuthConfig + ) + + expect(tokensModule.getTokens).toHaveBeenCalledWith( + requestClient, + mockAuthConfig + ) + }) + + it('should get a session from the session manager before executing', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context' + ) + + expect(sessionManager.getSession).toHaveBeenCalledWith(undefined) + }) + + it('should handle errors while getting a session', async () => { + jest + .spyOn(sessionManager, 'getSession') + .mockImplementation(() => Promise.reject('Test Error')) + + const error = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context' + ).catch((e) => e) + + expect(error.includes('Error while getting session.')).toBeTruthy() + }) + + it('should fetch the PID when printPid is true', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context', + mockAuthConfig, + null, + false, + false, + false, + defaultPollOptions, + true + ) + + expect(sessionManager.getVariable).toHaveBeenCalledWith( + mockSession.id, + 'SYSJOBID', + mockAuthConfig.access_token + ) + }) + + it('should handle errors while getting the job PID', async () => { + jest + .spyOn(sessionManager, 'getVariable') + .mockImplementation(() => Promise.reject('Test Error')) + + const error = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context', + mockAuthConfig, + null, + false, + false, + false, + defaultPollOptions, + true + ).catch((e) => e) + + expect(error.includes('Error while getting session variable.')).toBeTruthy() + }) + + it('should use the file upload approach when data contains semicolons', async () => { + jest + .spyOn(uploadTablesModule, 'uploadTables') + .mockImplementation(() => + Promise.resolve([{ tableName: 'test', file: { id: 1 } }]) + ) + + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context', + mockAuthConfig, + { foo: 'bar;' }, + false, + false, + false, + defaultPollOptions, + true + ) + + expect(uploadTablesModule.uploadTables).toHaveBeenCalledWith( + requestClient, + { foo: 'bar;' }, + mockAuthConfig.access_token + ) + }) + + it('should format data as CSV when it does not contain semicolons', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put hello'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + false, + false, + defaultPollOptions, + true + ) + + expect(formatDataModule.formatDataForRequest).toHaveBeenCalledWith({ + foo: 'bar' + }) + }) + + it('should submit a job for execution via the compute API', async () => { + jest + .spyOn(formatDataModule, 'formatDataForRequest') + .mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' })) + + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + false, + false, + defaultPollOptions, + true + ) + + expect(requestClient.post).toHaveBeenCalledWith( + `/compute/sessions/${mockSession.id}/jobs`, + { + name: 'exec-test', + description: 'Powered by SASjs', + code: ['%put "hello";'], + variables: { + SYS_JES_JOB_URI: '', + _program: 'test/test', + sasjs_tables: 'foo', + sasjs0data: 'bar' + }, + arguments: { + _contextName: 'test context', + _OMITJSONLISTING: true, + _OMITJSONLOG: true, + _OMITSESSIONRESULTS: true, + _OMITTEXTLISTING: true, + _OMITTEXTLOG: true + } + }, + mockAuthConfig.access_token + ) + }) + + it('should set the correct variables when debug is true', async () => { + jest + .spyOn(formatDataModule, 'formatDataForRequest') + .mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' })) + + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + true, + false, + false, + defaultPollOptions, + true + ) + + expect(requestClient.post).toHaveBeenCalledWith( + `/compute/sessions/${mockSession.id}/jobs`, + { + name: 'exec-test', + description: 'Powered by SASjs', + code: ['%put "hello";'], + variables: { + SYS_JES_JOB_URI: '', + _program: 'test/test', + sasjs_tables: 'foo', + sasjs0data: 'bar', + _DEBUG: 131 + }, + arguments: { + _contextName: 'test context', + _OMITJSONLISTING: true, + _OMITJSONLOG: true, + _OMITSESSIONRESULTS: false, + _OMITTEXTLISTING: true, + _OMITTEXTLOG: false + } + }, + mockAuthConfig.access_token + ) + }) + + it('should handle errors during job submission', async () => { + jest + .spyOn(requestClient, 'post') + .mockImplementation(() => Promise.reject('Test Error')) + + const error = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + true, + false, + false, + defaultPollOptions, + true + ).catch((e) => e) + + console.log(error) + expect(error.includes('Error while posting job')).toBeTruthy() + }) + + it('should immediately return the session when waitForResult is false', async () => { + const result = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + true, + false, + false, + defaultPollOptions, + true + ) + + expect(result).toEqual(mockSession) + }) + + it('should poll for job completion when waitForResult is true', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + false, + true, + defaultPollOptions, + true + ) + + expect(pollJobStateModule.pollJobState).toHaveBeenCalledWith( + requestClient, + mockJob, + false, + '', + mockAuthConfig, + defaultPollOptions + ) + }) + + it('should handle general errors when polling for job status', async () => { + jest + .spyOn(pollJobStateModule, 'pollJobState') + .mockImplementation(() => Promise.reject('Poll Error')) + + const error = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + false, + true, + defaultPollOptions, + true + ).catch((e) => e) + + expect(error.includes('Error while polling job status.')).toBeTruthy() + }) + + it('should fetch the log and append it to the error in case of a 5113 error code', async () => { + jest + .spyOn(pollJobStateModule, 'pollJobState') + .mockImplementation(() => + Promise.reject({ response: { data: 'err=5113,' } }) + ) + + const error = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + false, + true, + defaultPollOptions, + true + ).catch((e) => e) + + expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith( + requestClient, + mockAuthConfig.access_token, + mockJob.links.find((l) => l.rel === 'up')!.href + '/log', + 1000000 + ) + expect(error.log).toEqual('Test Log') + }) + + it('should fetch the logs for the job if debug is true and a log URL is available', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + true, + false, + true, + defaultPollOptions, + true + ) + + expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith( + requestClient, + mockAuthConfig.access_token, + mockJob.links.find((l) => l.rel === 'log')!.href + '/content', + mockJob.logStatistics.lineCount + ) + }) + + it('should not fetch the logs for the job if debug is false', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + false, + true, + defaultPollOptions, + true + ) + + expect(fetchLogsModule.fetchLogByChunks).not.toHaveBeenCalled() + }) + + it('should throw a ComputeJobExecutionError if the job has failed', async () => { + jest + .spyOn(pollJobStateModule, 'pollJobState') + .mockImplementation(() => Promise.resolve('failed')) + + const error: ComputeJobExecutionError = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + true, + false, + true, + defaultPollOptions, + true + ).catch((e) => e) + + expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith( + requestClient, + mockAuthConfig.access_token, + mockJob.links.find((l) => l.rel === 'log')!.href + '/content', + mockJob.logStatistics.lineCount + ) + + expect(error).toBeInstanceOf(ComputeJobExecutionError) + expect(error.log).toEqual('Test Log') + expect(error.job).toEqual(mockJob) + }) + + it('should throw a ComputeJobExecutionError if the job has errored out', async () => { + jest + .spyOn(pollJobStateModule, 'pollJobState') + .mockImplementation(() => Promise.resolve('error')) + + const error: ComputeJobExecutionError = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + true, + false, + true, + defaultPollOptions, + true + ).catch((e) => e) + + expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith( + requestClient, + mockAuthConfig.access_token, + mockJob.links.find((l) => l.rel === 'log')!.href + '/content', + mockJob.logStatistics.lineCount + ) + + expect(error).toBeInstanceOf(ComputeJobExecutionError) + expect(error.log).toEqual('Test Log') + expect(error.job).toEqual(mockJob) + }) + + it('should fetch the result if expectWebout is true', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + true, + true, + defaultPollOptions, + true + ) + + expect(requestClient.get).toHaveBeenCalledWith( + `/compute/sessions/${mockSession.id}/filerefs/_webout/content`, + mockAuthConfig.access_token, + 'text/plain' + ) + }) + + it('should fetch the logs if the webout file was not found', async () => { + jest.spyOn(requestClient, 'get').mockImplementation((url, ...rest) => { + if (url.includes('_webout')) { + return Promise.reject(new NotFoundError(url)) + } + return Promise.resolve({ result: mockJob, etag: '' }) + }) + + const error = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + true, + true, + defaultPollOptions, + true + ).catch((e) => e) + + expect(requestClient.get).toHaveBeenCalledWith( + `/compute/sessions/${mockSession.id}/filerefs/_webout/content`, + mockAuthConfig.access_token, + 'text/plain' + ) + + expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith( + requestClient, + mockAuthConfig.access_token, + mockJob.links.find((l) => l.rel === 'log')!.href + '/content', + mockJob.logStatistics.lineCount + ) + + expect(error.status).toEqual(500) + expect(error.log).toEqual('Test Log') + }) + + it('should clear the session after execution is complete', async () => { + await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + true, + true, + defaultPollOptions, + true + ) + + expect(sessionManager.clearSession).toHaveBeenCalledWith( + mockSession.id, + mockAuthConfig.access_token + ) + }) + + it('should handle errors while clearing a session', async () => { + jest + .spyOn(sessionManager, 'clearSession') + .mockImplementation(() => Promise.reject('Clear Session Error')) + + const error = await executeScript( + requestClient, + sessionManager, + 'test', + 'test', + ['%put "hello";'], + 'test context', + mockAuthConfig, + { foo: 'bar' }, + false, + true, + true, + defaultPollOptions, + true + ).catch((e) => { + console.log(e) + return e + }) + + expect(error.includes('Error while clearing session.')).toBeTruthy() + }) +}) + +const setupMocks = () => { + jest.restoreAllMocks() + jest.mock('../../../request/RequestClient') + jest.mock('../../../SessionManager') + jest.mock('../../../auth/tokens') + jest.mock('../pollJobState') + jest.mock('../uploadTables') + jest.mock('../../../utils/formatDataForRequest') + jest.mock('../../../utils/fetchLogByChunks') + + jest + .spyOn(requestClient, 'post') + .mockImplementation(() => Promise.resolve({ result: mockJob, etag: '' })) + jest + .spyOn(requestClient, 'get') + .mockImplementation(() => Promise.resolve({ result: mockJob, etag: '' })) + jest + .spyOn(requestClient, 'delete') + .mockImplementation(() => Promise.resolve({ result: {}, etag: '' })) + jest + .spyOn(tokensModule, 'getTokens') + .mockImplementation(() => Promise.resolve(mockAuthConfig)) + jest + .spyOn(pollJobStateModule, 'pollJobState') + .mockImplementation(() => Promise.resolve('completed')) + jest + .spyOn(sessionManager, 'getVariable') + .mockImplementation(() => + Promise.resolve({ result: { value: 'test' }, etag: 'test' }) + ) + jest + .spyOn(sessionManager, 'getSession') + .mockImplementation(() => Promise.resolve(mockSession)) + jest + .spyOn(sessionManager, 'clearSession') + .mockImplementation(() => Promise.resolve()) + jest + .spyOn(formatDataModule, 'formatDataForRequest') + .mockImplementation(() => ({ sasjs_tables: 'test', sasjs0data: 'test' })) + jest + .spyOn(fetchLogsModule, 'fetchLogByChunks') + .mockImplementation(() => Promise.resolve('Test Log')) +} diff --git a/src/api/viya/spec/mockResponses.ts b/src/api/viya/spec/mockResponses.ts new file mode 100644 index 0000000..e85028c --- /dev/null +++ b/src/api/viya/spec/mockResponses.ts @@ -0,0 +1,56 @@ +import { AuthConfig } from '@sasjs/utils/types' +import { Job, Session } from '../../../types' + +export const mockSession: Session = { + id: 's35510n', + state: 'idle', + links: [], + attributes: { + sessionInactiveTimeout: 1 + }, + creationTimeStamp: new Date().valueOf().toString() +} + +export const mockJob: Job = { + id: 'j0b', + name: 'test job', + uri: '/j0b', + createdBy: 'test user', + results: { + '_webout.json': 'test' + }, + logStatistics: { + lineCount: 100, + modifiedTimeStamp: new Date().valueOf().toString() + }, + links: [ + { + rel: 'log', + href: '/log', + method: 'GET', + type: 'log', + uri: 'log' + }, + { + rel: 'state', + href: '/state', + method: 'GET', + type: 'state', + uri: 'state' + }, + { + rel: 'up', + href: '/job', + method: 'GET', + type: 'up', + uri: 'job' + } + ] +} + +export const mockAuthConfig: AuthConfig = { + client: 'cl13nt', + secret: '53cr3t', + access_token: 'acc355', + refresh_token: 'r3fr35h' +}