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 getTokensModule from '../../../auth/getTokens' 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(getTokensModule.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(getTokensModule.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: any) => e) expect(error).toContain('Error while getting session.') }) 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: any) => e) expect(error).toContain('Error while getting session variable.') }) 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: any) => e) expect(error).toContain('Error while posting job') }) 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: any) => e) expect(error).toContain('Error while polling job status.') }) 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: any) => 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: any) => 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: any) => 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: '', status: 200 }) }) const error = await executeScript( requestClient, sessionManager, 'test', 'test', ['%put "hello";'], 'test context', mockAuthConfig, { foo: 'bar' }, false, true, true, defaultPollOptions, true ).catch((e: any) => 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: any) => e) expect(error).toContain('Error while clearing session.') }) }) const setupMocks = () => { jest.restoreAllMocks() jest.mock('../../../request/RequestClient') jest.mock('../../../SessionManager') jest.mock('../../../auth/getTokens') 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: '', status: 200 }) ) jest .spyOn(requestClient, 'delete') .mockImplementation(() => Promise.resolve({ result: {}, etag: '' })) jest .spyOn(getTokensModule, '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', status: 200 }) ) 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')) }