1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 01:14:36 +00:00

chore: better test coverage

This commit is contained in:
2025-03-04 14:48:22 +01:00
parent 18be9e8806
commit b3b2c1414c
5 changed files with 487 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
/**
* @jest-environment node
*/
import * as https from 'https'
import NodeFormData from 'form-data'
import { SAS9ApiClient } from '../SAS9ApiClient'
import { Sas9RequestClient } from '../request/Sas9RequestClient'
// Mock the Sas9RequestClient so that we can control its behavior
jest.mock('../request/Sas9RequestClient', () => {
return {
Sas9RequestClient: jest
.fn()
.mockImplementation(
(serverUrl: string, httpsAgentOptions?: https.AgentOptions) => {
return {
login: jest.fn().mockResolvedValue(undefined),
post: jest.fn().mockResolvedValue({ result: 'execution result' })
}
}
)
}
})
describe('SAS9ApiClient', () => {
const serverUrl = 'http://test-server.com'
const jobsPath = '/SASStoredProcess/do'
let client: SAS9ApiClient
let mockRequestClient: any
beforeEach(() => {
client = new SAS9ApiClient(serverUrl, jobsPath)
// Retrieve the instance of the mocked Sas9RequestClient
mockRequestClient = (Sas9RequestClient as jest.Mock).mock.results[0].value
})
afterEach(() => {
jest.clearAllMocks()
})
describe('getConfig', () => {
it('should return the correct configuration', () => {
const config = client.getConfig()
expect(config).toEqual({ serverUrl })
})
})
describe('setConfig', () => {
it('should update the serverUrl when a valid value is provided', () => {
const newUrl = 'http://new-server.com'
client.setConfig(newUrl)
expect(client.getConfig()).toEqual({ serverUrl: newUrl })
})
it('should not update the serverUrl when an empty string is provided', () => {
const originalConfig = client.getConfig()
client.setConfig('')
expect(client.getConfig()).toEqual(originalConfig)
})
})
describe('executeScript', () => {
const linesOfCode = ['line1;', 'line2;']
const userName = 'testUser'
const password = 'testPass'
const fixedTimestamp = '1234567890'
const expectedFilename = `sasjs-execute-sas9-${fixedTimestamp}.sas`
beforeAll(() => {
// Stub generateTimestamp so that we get a consistent filename in our tests.
jest
.spyOn(require('@sasjs/utils/time'), 'generateTimestamp')
.mockReturnValue(fixedTimestamp)
})
afterAll(() => {
jest.restoreAllMocks()
})
it('should execute the script and return the result', async () => {
const result = await client.executeScript(linesOfCode, userName, password)
// Verify that login is called with the correct parameters.
expect(mockRequestClient.login).toHaveBeenCalledWith(
userName,
password,
jobsPath
)
// Build the expected stored process URL.
const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
const expectedUrl =
`${jobsPath}/?` + '_program=' + codeInjectorPath + '&_debug=log'
// Verify that post was called with the expected stored process URL.
expect(mockRequestClient.post).toHaveBeenCalledWith(
expectedUrl,
expect.any(NodeFormData),
undefined,
expect.stringContaining('multipart/form-data; boundary='),
expect.objectContaining({
'Content-Length': expect.any(Number),
'Content-Type': expect.stringContaining(
'multipart/form-data; boundary='
),
Accept: '*/*'
})
)
// The method should return the result from the post call.
expect(result).toEqual('execution result')
})
it('should include the force output code in the uploaded form data', async () => {
await client.executeScript(linesOfCode, userName, password)
// Retrieve the form data passed to post
const postCallArgs = (mockRequestClient.post as jest.Mock).mock.calls[0]
const formData: NodeFormData = postCallArgs[1]
// We can inspect the boundary and ensure that the filename was generated correctly.
expect(formData.getBoundary()).toBeDefined()
// The filename is used as the key for the form field.
const formDataBuffer = formData.getBuffer().toString()
expect(formDataBuffer).toContain(expectedFilename)
// Also check that the force output code is appended.
expect(formDataBuffer).toContain("put 'Executed sasjs run';")
})
})
})

View File

@@ -0,0 +1,231 @@
import NodeFormData from 'form-data'
import {
SASjsApiClient,
SASjsAuthResponse,
ScriptExecutionResult
} from '../SASjsApiClient'
import { AuthConfig, ServicePackSASjs } from '@sasjs/utils/types'
import { ExecutionQuery } from '../types'
// Create a mock request client with a post method.
const mockPost = jest.fn()
const mockRequestClient = {
post: mockPost
}
// Instead of referencing external variables, inline the dummy values in the mock factories.
jest.mock('../auth/getTokens', () => ({
getTokens: jest.fn().mockResolvedValue({ access_token: 'dummyAccessToken' })
}))
jest.mock('../auth/getAccessTokenForSasjs', () => ({
getAccessTokenForSasjs: jest.fn().mockResolvedValue({
access_token: 'newAccessToken',
refresh_token: 'newRefreshToken'
} as any)
}))
jest.mock('../auth/refreshTokensForSasjs', () => ({
refreshTokensForSasjs: jest.fn().mockResolvedValue({
access_token: 'newAccessToken',
refresh_token: 'newRefreshToken'
} as any)
}))
// For deployZipFile, mock the file reading function.
jest.mock('@sasjs/utils/file', () => ({
createReadStream: jest.fn().mockResolvedValue('readStreamDummy')
}))
// Dummy result to compare against.
const dummyResult = {
status: 'OK',
message: 'Success',
streamServiceName: 'service',
example: {}
}
describe('SASjsApiClient', () => {
let client: SASjsApiClient
beforeEach(() => {
client = new SASjsApiClient(mockRequestClient as any)
mockPost.mockReset()
})
describe('deploy', () => {
it('should deploy service pack using JSON', async () => {
// Arrange: Simulate a successful response.
mockPost.mockResolvedValue({ result: dummyResult })
const dataJson: ServicePackSASjs = {
appLoc: '',
someOtherProp: 'value'
} as any
const appLoc = '/base/appLoc'
const authConfig: AuthConfig = {
client: 'clientId',
secret: 'secret',
access_token: 'token',
refresh_token: 'refresh'
}
// Act
const result = await client.deploy(dataJson, appLoc, authConfig)
// Assert: Ensure that the JSON gets the appLoc set if not defined.
expect(dataJson.appLoc).toBe(appLoc)
expect(mockPost).toHaveBeenCalledWith(
'SASjsApi/drive/deploy',
dataJson,
'dummyAccessToken',
undefined,
{},
{ maxContentLength: Infinity, maxBodyLength: Infinity }
)
expect(result).toEqual(dummyResult)
})
})
describe('deployZipFile', () => {
it('should deploy zip file and return the result', async () => {
// Arrange: Simulate a successful response.
mockPost.mockResolvedValue({ result: dummyResult })
const zipFilePath = 'path/to/deploy.zip'
const authConfig: AuthConfig = {
client: 'clientId',
secret: 'secret',
access_token: 'token',
refresh_token: 'refresh'
}
// Act
const result = await client.deployZipFile(zipFilePath, authConfig)
// Assert: Verify that POST is called with multipart form-data.
expect(mockPost).toHaveBeenCalled()
const callArgs = mockPost.mock.calls[0]
expect(callArgs[0]).toBe('SASjsApi/drive/deploy/upload')
expect(result).toEqual(dummyResult)
})
})
describe('executeJob', () => {
it('should execute a job with absolute program path', async () => {
// Arrange
const query: ExecutionQuery = { _program: '/absolute/path' } as any
const appLoc = '/base/appLoc'
const authConfig: AuthConfig = { access_token: 'anyToken' } as any
mockPost.mockResolvedValue({
result: { jobId: 123 },
log: 'execution log'
})
// Act
const { result, log } = await client.executeJob(query, appLoc, authConfig)
// Assert: The program path should not be prefixed.
expect(mockPost).toHaveBeenCalledWith(
'SASjsApi/stp/execute',
{ _debug: 131, ...query, _program: '/absolute/path' },
'anyToken'
)
expect(result).toEqual({ jobId: 123 })
expect(log).toBe('execution log')
})
it('should execute a job with relative program path', async () => {
// Arrange
const query: ExecutionQuery = { _program: 'relative/path' } as any
const appLoc = '/base/appLoc'
mockPost.mockResolvedValue({ result: { jobId: 456 }, log: 'another log' })
// Act
const { result, log } = await client.executeJob(query, appLoc)
// Assert: The program path should be prefixed with appLoc.
expect(mockPost).toHaveBeenCalledWith(
'SASjsApi/stp/execute',
{ _debug: 131, ...query, _program: '/base/appLoc/relative/path' },
undefined
)
expect(result).toEqual({ jobId: 456 })
expect(log).toBe('another log')
})
})
describe('executeScript', () => {
it('should execute a script and return the execution result', async () => {
// Arrange
const code = 'data _null_; run;'
const runTime = 'sas'
const authConfig: AuthConfig = {
client: 'clientId',
secret: 'secret',
access_token: 'token',
refresh_token: 'refresh'
}
const responsePayload = {
log: 'log output',
printOutput: 'print output',
result: 'web output'
}
mockPost.mockResolvedValue(responsePayload)
// Act
const result: ScriptExecutionResult = await client.executeScript(
code,
runTime,
authConfig
)
// Assert
expect(mockPost).toHaveBeenCalledWith(
'SASjsApi/code/execute',
{ code, runTime },
'dummyAccessToken'
)
expect(result.log).toBe('log output')
expect(result.printOutput).toBe('print output')
expect(result.webout).toBe('web output')
})
it('should throw an error with a prefixed message when POST fails', async () => {
// Arrange
const code = 'data _null_; run;'
const errorMessage = 'Network Error'
mockPost.mockRejectedValue(new Error(errorMessage))
// Act & Assert
await expect(client.executeScript(code)).rejects.toThrow(
/Error while sending POST request to execute code/
)
})
})
describe('getAccessToken', () => {
it('should exchange auth code for access token', async () => {
// Act
const result = await client.getAccessToken('clientId', 'authCode123')
// Assert: The result should match the dummy auth response.
expect(result).toEqual({
access_token: 'newAccessToken',
refresh_token: 'newRefreshToken'
})
})
})
describe('refreshTokens', () => {
it('should exchange refresh token for new tokens', async () => {
// Act
const result = await client.refreshTokens('refreshToken123')
// Assert: The result should match the dummy auth response.
expect(result).toEqual({
access_token: 'newAccessToken',
refresh_token: 'newRefreshToken'
})
})
})
})

View File

@@ -0,0 +1,30 @@
import { SAS9AuthError } from '../SAS9AuthError'
describe('SAS9AuthError', () => {
it('should have the correct error message', () => {
const error = new SAS9AuthError()
expect(error.message).toBe(
'The credentials you provided cannot be authenticated. Please provide a valid set of credentials.'
)
})
it('should have the correct error name', () => {
const error = new SAS9AuthError()
expect(error.name).toBe('AuthorizeError')
})
it('should be an instance of SAS9AuthError', () => {
const error = new SAS9AuthError()
expect(error).toBeInstanceOf(SAS9AuthError)
})
it('should be an instance of Error', () => {
const error = new SAS9AuthError()
expect(error).toBeInstanceOf(Error)
})
it('should set the prototype correctly', () => {
const error = new SAS9AuthError()
expect(Object.getPrototypeOf(error)).toBe(SAS9AuthError.prototype)
})
})

View File

@@ -0,0 +1,24 @@
import { parseSasViyaLog } from '../parseSasViyaLog'
describe('parseSasViyaLog', () => {
it('should parse sas viya log if environment is Node', () => {
const logResponse = {
items: [{ line: 'Line 1' }, { line: 'Line 2' }, { line: 'Line 3' }]
}
const expectedLog = 'Line 1\nLine 2\nLine 3'
const result = parseSasViyaLog(logResponse)
expect(result).toEqual(expectedLog)
})
it('should handle exceptions and return the original logResponse', () => {
// Create a logResponse that will cause an error in the mapping process.
const logResponse: any = {
items: null
}
// Since logResponse.items is null, the ternary operator returns the else branch.
const expectedLog = JSON.stringify(logResponse)
const result = parseSasViyaLog(logResponse)
expect(result).toEqual(expectedLog)
})
})

View File

@@ -0,0 +1,72 @@
import { RequestClient } from '../../request/RequestClient'
import { parseSasViyaDebugResponse } from '../parseViyaDebugResponse'
describe('parseSasViyaDebugResponse', () => {
let requestClient: RequestClient
const serverUrl = 'http://test-server.com'
beforeEach(() => {
requestClient = {
get: jest.fn()
} as unknown as RequestClient
})
it('should extract URL and call get for Viya 3.5 iframe style', async () => {
const iframeUrl = '/path/to/log.json'
const response = `<html><body><iframe style="width: 99%; height: 500px" src="${iframeUrl}"></iframe></body></html>`
const resultData = { message: 'success' }
// Mock the get method to resolve with an object containing the JSON result as string.
;(requestClient.get as jest.Mock).mockResolvedValue({
result: JSON.stringify(resultData)
})
const result = await parseSasViyaDebugResponse(
response,
requestClient,
serverUrl
)
expect(requestClient.get).toHaveBeenCalledWith(
serverUrl + iframeUrl,
undefined,
'text/plain'
)
expect(result).toEqual(resultData)
})
it('should extract URL and call get for Viya 4 iframe style', async () => {
const iframeUrl = '/another/path/to/log.json'
// Note: For Viya 4, the regex splits in such a way that the extracted URL includes an extra starting double-quote.
// For example, the URL becomes: '"/another/path/to/log.json'
const response = `<html><body><iframe style="width: 99%; height: 500px; background-color:Canvas;" src="${iframeUrl}"></iframe></body></html>`
const resultData = { status: 'ok' }
;(requestClient.get as jest.Mock).mockResolvedValue({
result: JSON.stringify(resultData)
})
const result = await parseSasViyaDebugResponse(
response,
requestClient,
serverUrl
)
// Expect the extra starting double-quote as per the current implementation.
const expectedUrl = serverUrl + `"` + iframeUrl
expect(requestClient.get).toHaveBeenCalledWith(
expectedUrl,
undefined,
'text/plain'
)
expect(result).toEqual(resultData)
})
it('should throw an error if iframe URL is not found', async () => {
const response = `<html><body>No iframe here</body></html>`
await expect(
parseSasViyaDebugResponse(response, requestClient, serverUrl)
).rejects.toThrow('Unable to find webout file URL.')
})
})