import { SessionManager } from '../SessionManager' import { RequestClient } from '../request/RequestClient' import * as dotenv from 'dotenv' import axios from 'axios' import { Logger, LogLevel } from '@sasjs/utils/logger' import { Session, SessionState, Context } from '../types' jest.mock('axios') const mockedAxios = axios as jest.Mocked const requestClient = new (>RequestClient)() describe('SessionManager', () => { dotenv.config() process.env.SERVER_URL = 'https://server.com' const sessionManager = new SessionManager( process.env.SERVER_URL as string, process.env.DEFAULT_COMPUTE_CONTEXT as string, requestClient ) const sessionStateLink = '/compute/sessions/session-id-ses0000/state' const sessionEtag = 'etag-string' const getMockSession = (): Session => ({ id: ['id', new Date().getTime(), Math.random()].join('-'), state: SessionState.NoState, links: [ { href: sessionStateLink, method: 'GET', rel: 'state', type: 'text/plain', uri: sessionStateLink } ], attributes: { sessionInactiveTimeout: 900 }, creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`, stateUrl: sessionStateLink, etag: sessionEtag }) afterEach(() => { jest.restoreAllMocks() }) describe('getVariable', () => { it('should fetch session variable', async () => { const sampleResponse = { ok: true, links: [], name: 'SYSJOBID', scope: 'GLOBAL', value: '25218', version: 1 } mockedAxios.get.mockImplementation(() => Promise.resolve({ data: sampleResponse }) ) const expectedResponse = { etag: '', result: sampleResponse } await expect( sessionManager.getVariable( 'fakeSessionId', 'SYSJOBID', 'fakeAccessToken' ) ).resolves.toEqual(expectedResponse) }) it('should throw an error if GET request failed', async () => { const responseStatus = 500 const responseErrorMessage = `The process timed out after 60 seconds. Request failed with status code ${responseStatus}` const response = { status: responseStatus, data: { message: responseErrorMessage } } const testVariable = 'testVariable' jest.spyOn(requestClient, 'get').mockImplementation(() => Promise.reject({ response }) ) const expectedError = `Error while fetching session variable '${testVariable}'. GET request to ${process.env.SERVER_URL}/compute/sessions/testId/variables/${testVariable} failed with status code ${responseStatus}. ${responseErrorMessage}` await expect( sessionManager.getVariable('testId', testVariable) ).rejects.toEqual(expectedError) }) }) describe('waitForSession', () => { const session: Session = { id: 'id', state: SessionState.NoState, links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }], attributes: { sessionInactiveTimeout: 0 }, creationTimeStamp: '', stateUrl: sessionStateLink, etag: sessionEtag } beforeEach(() => { ;(process as any).logger = new Logger(LogLevel.Off) }) it('should log http response code and session state if SAS server did not provide session state', async () => { let requestAttempt = 0 const requestAttemptLimit = 10 const sessionState = 'idle' mockedAxios.get.mockImplementation(() => { requestAttempt += 1 if (requestAttempt >= requestAttemptLimit) { return Promise.resolve({ data: sessionState, status: 200 }) } return Promise.resolve({ data: '', status: 304 }) }) jest.spyOn((process as any).logger, 'info') sessionManager.debug = true await expect( sessionManager['waitForSession'](session, null, 'access_token') ).resolves.toEqual(sessionState) const sessionStateUrl = process.env.SERVER_URL + session.stateUrl expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit) expect((process as any).logger.info).toHaveBeenCalledTimes(3) expect((process as any).logger.info).toHaveBeenNthCalledWith( 1, `Polling: ${sessionStateUrl}` ) expect((process as any).logger.info).toHaveBeenNthCalledWith( 2, `Could not get session state. Server responded with 304 whilst checking state: ${sessionStateUrl}` ) expect((process as any).logger.info).toHaveBeenNthCalledWith( 3, `Current session state is '${sessionState}'` ) }) it('should throw an error if there is no session link', async () => { const customSession = JSON.parse(JSON.stringify(session)) customSession.stateUrl = '' mockedAxios.get.mockImplementation(() => Promise.resolve({ data: customSession.state, status: 200 }) ) await expect( sessionManager['waitForSession'](customSession, null, 'access_token') ).rejects.toContain('Error while getting session state link.') }) it('should throw an error if could not get session state', async () => { const gettingSessionStatus = 500 const sessionStatusError = `Getting session status timed out after 60 seconds. Request failed with status code ${gettingSessionStatus}` const sessionStateUrl = process.env.SERVER_URL + session.stateUrl mockedAxios.get.mockImplementation(() => Promise.reject({ response: { status: gettingSessionStatus, data: { message: sessionStatusError } } }) ) const expectedError = `Error while waiting for session. Error while getting session state. GET request to ${sessionStateUrl}?wait=30 failed with status code ${gettingSessionStatus}. ${sessionStatusError}` await expect( sessionManager['waitForSession'](session, null, 'access_token') ).rejects.toEqual(expectedError) }) it('should return session state', async () => { const customSession = JSON.parse(JSON.stringify(session)) customSession.state = 'completed' mockedAxios.get.mockImplementation(() => Promise.resolve({ data: customSession.state, status: 200 }) ) await expect( sessionManager['waitForSession'](customSession, null, 'access_token') ).resolves.toEqual(customSession.state) }) }) describe('isSessionValid', () => { const session: Session = getMockSession() it('should return false if not a session provided', () => { expect(sessionManager['isSessionValid'](undefined as any)).toEqual(false) }) it('should return true if session is not expired', () => { expect(sessionManager['isSessionValid'](session)).toEqual(true) }) it('should return false if session is expired', () => { session.creationTimeStamp = `${new Date( new Date().getTime() - (session.attributes.sessionInactiveTimeout * 1000 + 1000) ).toISOString()}` expect(sessionManager['isSessionValid'](session)).toEqual(false) }) }) describe('removeSessionFromPool', () => { it('should remove session from the pool of sessions', () => { const session: Session = getMockSession() const sessions: Session[] = [getMockSession(), session] sessionManager['sessions'] = sessions sessionManager['removeSessionFromPool'](session) expect(sessionManager['sessions'].length).toEqual(1) }) }) describe('getSession', () => { it('should return session if there is a valid session and create new session', async () => { jest .spyOn(sessionManager as any, 'createAndWaitForSession') .mockImplementation(async () => Promise.resolve(getMockSession())) const session = getMockSession() sessionManager['sessions'] = [session] await expect(sessionManager.getSession()).resolves.toEqual(session) expect(sessionManager['createAndWaitForSession']).toHaveBeenCalled() }) it('should return a session and keep one session if there is no sessions available', async () => { jest .spyOn(sessionManager as any, 'createAndWaitForSession') .mockImplementation(async () => { const session = getMockSession() sessionManager['sessions'].push(session) return Promise.resolve(session) }) const session = await sessionManager.getSession() expect(Object.keys(session)).toEqual(Object.keys(getMockSession())) expect(sessionManager['createAndWaitForSession']).toHaveBeenCalledTimes(2) expect(sessionManager['sessions'].length).toEqual(1) }) it.concurrent( 'should throw an error if session creation request returned 500', async () => { const sessionCreationStatus = 500 const sessionCreationError = `The process initialization for the Compute server with the ID 'ed40398a-ec8a-422b-867a-61493ee8a57f' timed out after 60 seconds. Request failed with status code ${sessionCreationStatus}` jest.spyOn(requestClient, 'post').mockImplementation(() => Promise.reject({ response: { status: sessionCreationStatus, data: { message: sessionCreationError } } }) ) const contextId = 'testContextId' const context: Context = { name: 'testContext', id: contextId, createdBy: 'createdBy', version: 1 } sessionManager['currentContext'] = context const expectedError = new Error( `Error while creating session. POST request to ${process.env.SERVER_URL}/compute/contexts/${contextId}/sessions failed with status code ${sessionCreationStatus}. ${sessionCreationError}` ) await expect(sessionManager.getSession()).rejects.toEqual(expectedError) } ) }) describe('clearSession', () => { it('should clear session', async () => { jest .spyOn(requestClient, 'delete') .mockImplementation(() => Promise.resolve({ result: '', etag: '', status: 200 }) ) const sessionToBeCleared = getMockSession() const sessionToStay = getMockSession() sessionManager['sessions'] = [sessionToBeCleared, sessionToStay] await sessionManager.clearSession(sessionToBeCleared.id) expect(sessionManager['sessions']).toEqual([sessionToStay]) }) it('should throw error if DELETE request failed', async () => { const sessionCreationStatus = 500 const sessionDeleteError = `The process timed out after 60 seconds. Request failed with status code ${sessionCreationStatus}` jest.spyOn(requestClient, 'delete').mockImplementation(() => Promise.reject({ response: { status: sessionCreationStatus, data: { message: sessionDeleteError } } }) ) const session = getMockSession() sessionManager['sessions'] = [session] const expectedError = `Error while deleting session. DELETE request to /compute/sessions/${session.id} failed with status code ${sessionCreationStatus}. ${sessionDeleteError}` await expect(sessionManager.clearSession(session.id)).rejects.toEqual( expectedError ) }) }) describe('waitForCurrentContext', () => { it('should resolve when current context is ready', async () => { sessionManager['settingContext'] = true sessionManager['contextName'] = 'test context' await expect(sessionManager['waitForCurrentContext']()).toResolve() expect(sessionManager['settingContext']).toEqual(false) }) }) describe('setCurrentContext', () => { it('should set current context', async () => { const contextName = 'test context' const testContext: Context = { name: contextName, id: 'string', createdBy: 'string', version: 1 } jest.spyOn(requestClient, 'get').mockImplementation(() => { return Promise.resolve({ result: { items: [testContext] }, etag: '', status: 200 }) }) sessionManager['currentContext'] = null sessionManager['contextName'] = contextName sessionManager['settingContext'] = false await expect(sessionManager['setCurrentContext']()).toResolve() expect(sessionManager['currentContext']).toEqual(testContext) }) it('should throw error if GET request failed', async () => { const responseStatus = 500 const responseErrorMessage = `The process timed out after 60 seconds. Request failed with status code ${responseStatus}` const response = { status: responseStatus, data: { message: responseErrorMessage } } jest.spyOn(requestClient, 'get').mockImplementation(() => Promise.reject({ response }) ) const expectedError = `Error while getting list of contexts. GET request to ${process.env.SERVER_URL}/compute/contexts?limit=10000 failed with status code ${responseStatus}. ${responseErrorMessage}` sessionManager['currentContext'] = null await expect(sessionManager['setCurrentContext']()).rejects.toEqual( expectedError ) }) it('should throw an error if current context is not in the list of contexts', async () => { const contextName = 'test context' const testContext: Context = { name: `${contextName} does not exist`, id: 'string', createdBy: 'string', version: 1 } jest.spyOn(requestClient, 'get').mockImplementation(() => { return Promise.resolve({ result: { items: [testContext] }, etag: '', status: 200 }) }) sessionManager['currentContext'] = null sessionManager['contextName'] = contextName sessionManager['settingContext'] = false const expectedError = new Error( `The context '${contextName}' was not found on the server ${process.env.SERVER_URL}.` ) await expect(sessionManager['setCurrentContext']()).rejects.toEqual( expectedError ) }) }) describe('createAndWaitForSession', () => { it('should create session with etag and stateUrl', async () => { const etag = sessionEtag const customSession: any = getMockSession() delete customSession.etag delete customSession.stateUrl jest.spyOn(requestClient, 'post').mockImplementation(() => Promise.resolve({ result: customSession, etag }) ) jest .spyOn(sessionManager as any, 'setCurrentContext') .mockImplementation(() => Promise.resolve()) sessionManager['currentContext'] = { name: 'context name', id: 'string', createdBy: 'string', version: 1 } jest .spyOn(sessionManager as any, 'getSessionState') .mockImplementation(() => Promise.resolve({ result: SessionState.Idle, responseStatus: 200 }) ) const expectedSession = await sessionManager['createAndWaitForSession']() expect(customSession.id).toEqual(expectedSession.id) expect( customSession.links.find((l: any) => l.rel === 'state').href ).toEqual(expectedSession.stateUrl) expect(expectedSession.etag).toEqual(etag) }) }) })