diff --git a/.git-hooks/commit-msg b/.git-hooks/commit-msg index 003f76b..cb3bd89 100755 --- a/.git-hooks/commit-msg +++ b/.git-hooks/commit-msg @@ -6,7 +6,7 @@ GREEN="\033[1;32m" # temporary file which holds the message). commit_message=$(cat "$1") -if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-\*]+\))?!?: .+$") then +if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 -\*]+\))?!?: .+$") then echo "${GREEN} ✔ Commit message meets Conventional Commit standards" exit 0 fi diff --git a/src/SASViyaApiClient.spec.ts b/src/SASViyaApiClient.spec.ts new file mode 100644 index 0000000..4063817 --- /dev/null +++ b/src/SASViyaApiClient.spec.ts @@ -0,0 +1,51 @@ +import { Logger, LogLevel } from '@sasjs/utils/logger' +import { RequestClient } from './request/RequestClient' +import { SASViyaApiClient } from './SASViyaApiClient' +import { Folder } from './types' +import { RootFolderNotFoundError } from './types/errors' + +const mockFolder: Folder = { + id: '1', + uri: '/folder', + links: [], + memberCount: 1 +} + +const requestClient = new (>RequestClient)() +const sasViyaApiClient = new SASViyaApiClient( + 'https://test.com', + '/test', + 'test context', + requestClient +) + +describe('SASViyaApiClient', () => { + beforeEach(() => { + ;(process as any).logger = new Logger(LogLevel.Off) + setupMocks() + }) + + it('should throw an error when the root folder is not found on the server', async () => { + jest + .spyOn(requestClient, 'get') + .mockImplementation(() => Promise.reject('Not Found')) + const error = await sasViyaApiClient + .createFolder('test', '/foo') + .catch((e) => e) + expect(error).toBeInstanceOf(RootFolderNotFoundError) + }) +}) + +const setupMocks = () => { + jest + .spyOn(requestClient, 'get') + .mockImplementation(() => + Promise.resolve({ result: mockFolder, etag: '', status: 200 }) + ) + + jest + .spyOn(requestClient, 'post') + .mockImplementation(() => + Promise.resolve({ result: mockFolder, etag: '', status: 200 }) + ) +} diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index e68082c..a8dbc9f 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -11,7 +11,7 @@ import { JobDefinition, PollOptions } from './types' -import { JobExecutionError } from './types/errors' +import { JobExecutionError, RootFolderNotFoundError } from './types/errors' import { SessionManager } from './SessionManager' import { ContextManager } from './ContextManager' import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types' @@ -381,7 +381,11 @@ export class SASViyaApiClient { ) const newFolderName = `${parentFolderPath.split('/').pop()}` if (newParentFolderPath === '') { - throw new Error('Root folder has to be present on the server.') + throw new RootFolderNotFoundError( + parentFolderPath, + this.serverUrl, + accessToken + ) } logger.info( `Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'` diff --git a/src/types/errors/RootFolderNotFoundError.spec.ts b/src/types/errors/RootFolderNotFoundError.spec.ts new file mode 100644 index 0000000..a27e071 --- /dev/null +++ b/src/types/errors/RootFolderNotFoundError.spec.ts @@ -0,0 +1,40 @@ +import { RootFolderNotFoundError } from './RootFolderNotFoundError' + +describe('RootFolderNotFoundError', () => { + it('when access token is provided, error message should contain the scopes in the token', () => { + const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzY29wZS0xIiwic2NvcGUtMiJdfQ.ktqPL2ulln-8Asa2jSV9QCfDYmQuNk4tNKopxJR5xZs' + + const error = new RootFolderNotFoundError( + '/myProject', + 'https://analytium.co.uk', + token + ) + + expect(error).toBeInstanceOf(RootFolderNotFoundError) + expect(error.message).toContain('scope-1') + expect(error.message).toContain('scope-2') + }) + + it('when access token is not provided, error message should not contain scopes', () => { + const error = new RootFolderNotFoundError( + '/myProject', + 'https://analytium.co.uk' + ) + + expect(error).toBeInstanceOf(RootFolderNotFoundError) + expect(error.message).not.toContain( + 'Your access token contains the following scopes' + ) + }) + + it('should include the folder path and SASDrive URL in the message', () => { + const folderPath = '/myProject' + const serverUrl = 'https://analytium.co.uk' + const error = new RootFolderNotFoundError(folderPath, serverUrl) + + expect(error).toBeInstanceOf(RootFolderNotFoundError) + expect(error.message).toContain(folderPath) + expect(error.message).toContain(`${serverUrl}/SASDrive`) + }) +}) diff --git a/src/types/errors/RootFolderNotFoundError.ts b/src/types/errors/RootFolderNotFoundError.ts new file mode 100644 index 0000000..f5f032e --- /dev/null +++ b/src/types/errors/RootFolderNotFoundError.ts @@ -0,0 +1,24 @@ +import { decodeToken } from '@sasjs/utils/auth' + +export class RootFolderNotFoundError extends Error { + constructor( + parentFolderPath: string, + serverUrl: string, + accessToken?: string + ) { + let message: string = + `Root folder ${parentFolderPath} was not found.` + + `\nPlease check ${serverUrl}/SASDrive.` + + `\nIf the folder DOES exist then it is likely a permission problem.\n` + if (accessToken) { + const decodedToken = decodeToken(accessToken) + let scope = decodedToken.scope + scope = scope.map((element) => '* ' + element) + message += + `Your access token contains the following scopes:\n` + scope.join('\n') + } + super(message) + this.name = 'RootFolderNotFoundError' + Object.setPrototypeOf(this, RootFolderNotFoundError.prototype) + } +} diff --git a/src/types/errors/index.ts b/src/types/errors/index.ts index f8595d4..cca1a97 100644 --- a/src/types/errors/index.ts +++ b/src/types/errors/index.ts @@ -7,3 +7,4 @@ export * from './LoginRequiredError' export * from './NotFoundError' export * from './ErrorResponse' export * from './NoSessionStateError' +export * from './RootFolderNotFoundError'