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:
130
src/spec/SAS9ApiClient.spec.ts
Normal file
130
src/spec/SAS9ApiClient.spec.ts
Normal 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';")
|
||||
})
|
||||
})
|
||||
})
|
||||
231
src/spec/SASjsApiClient.spec.ts
Normal file
231
src/spec/SASjsApiClient.spec.ts
Normal 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'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
30
src/types/errors/spec/SAS9AuthError.spec.ts
Normal file
30
src/types/errors/spec/SAS9AuthError.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
24
src/utils/spec/parseSasViyaLog.spec.ts
Normal file
24
src/utils/spec/parseSasViyaLog.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
72
src/utils/spec/parseViyaDebugResponse.spec.ts
Normal file
72
src/utils/spec/parseViyaDebugResponse.spec.ts
Normal 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.')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user