Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 86 additions & 27 deletions bin/__tests__/getIntegrationAccessToken.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ const getArguments = o => {
);
};

const mockServerErrorResponse = (message, code) => {
let newError = new Error(message);
newError.code = code;
return newError;
}

const mockUnhandledException = message => {
// this error has no "code" attribute
return new Error(message);
}

describe('getIntegrationAccessToken', () => {
let getIntegrationAccessToken;
let mockInquirer;
Expand Down Expand Up @@ -167,13 +178,12 @@ describe('getIntegrationAccessToken', () => {
});

it('reports error retrieving access token', async () => {
const mockedAuthError = 'some error: Bad things happened.'
const mockedAuthError = mockServerErrorResponse('some error: Bad things happened', 'server_error_code');
mockAuth.and.returnValue(
Promise.reject(new Error(mockedAuthError))
Promise.reject(mockedAuthError)
);

let errorMessage;

let returnedError;
try {
await getIntegrationAccessToken(
{
Expand All @@ -182,28 +192,29 @@ describe('getIntegrationAccessToken', () => {
getArguments()
);
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

// we bailed after the first call because it wasn't a scoping error
expect(mockAuth.calls.count()).toBe(1)

expect(errorMessage).toBe(
`Error retrieving access token. ${mockedAuthError}`
);
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: some error: Bad things happened')).toBeTrue();
expect(returnedError.message.includes('Error Code: server_error_code')).toBeTrue();
expect(returnedError.code).toBe('server_error_code');
});

it('attempts authenticating with each supported metascope', async () => {
const mockedAuthError = 'invalid_scope: Invalid metascope.';
const mockedAuthError = mockServerErrorResponse('invalid_scope: Invalid metascope.', 'invalid_scope');
mockAuth.and.returnValue(
Promise.reject(new Error(mockedAuthError))
Promise.reject(mockedAuthError)
);

let errorMessage;
let returnedError;
try {
await getIntegrationAccessToken(getEnvConfig(), getArguments());
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

expect(mockAuth).toHaveBeenCalledWith(
Expand All @@ -230,37 +241,84 @@ describe('getIntegrationAccessToken', () => {

expect(mockAuth.calls.count()).toBe(METASCOPES.length);
// This tests that if all metascopes fail, the error from the last attempt is ultimately thrown.
expect(errorMessage).toBe(
`Error retrieving access token. ${mockedAuthError}`
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: invalid_scope: Invalid metascope.')).toBeTrue();
expect(returnedError.message.includes('Error Code: invalid_scope')).toBeTrue();
expect(returnedError.code).toBe('invalid_scope');
});

it('throws a stack trace when error.code is missing', async () => {
const mockedAuthError = mockUnhandledException('500 server error');
mockAuth.and.returnValue(
Promise.reject(mockedAuthError)
);

let returnedError;
try {
// should be going through a bunch of scopes
await getIntegrationAccessToken(getEnvConfig(), getArguments());
} catch (error) {
returnedError = error;
}

// however, when we don't see an error.code, we bail and report
expect(mockAuth.calls.count()).toBe(1);
// the message should not have any of our pretty formatting
expect(returnedError.message).toBe('500 server error');
expect(returnedError.code).toBeFalsy();
});

it('throws a stack trace when --verbose and request_failed', async () => {
const mockedAuthError = mockServerErrorResponse('some error', 'request_failed');
mockAuth.and.returnValue(
Promise.reject(mockedAuthError)
);

let returnedError;
try {
// should be going through a bunch of scopes
await getIntegrationAccessToken(getEnvConfig(), getArguments({ verbose: true }));
} catch (error) {
returnedError = error;
}

// however, when we don't see an error.code, we bail and report
expect(mockAuth.calls.count()).toBe(1);
// the message should not have any of our pretty formatting
expect(returnedError.message).toBe('some error');
expect(returnedError.code).toBe('request_failed');
});

it('shows JS error details in case they happen', async () => {
const mockedAuthError = 'some error';
const mockedAuthError = mockServerErrorResponse('some error', 'server_error_code');
mockAuth.and.returnValue(
Promise.reject(new Error(mockedAuthError))
Promise.reject(mockedAuthError)
);

let errorMessage;
let returnedError;
try {
await getIntegrationAccessToken(getEnvConfig(), getArguments());
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

// we bailed after the first call because it wasn't a scoping error
expect(mockAuth.calls.count()).toBe(1)

expect(errorMessage).toBe(
`Error retrieving access token. ${mockedAuthError}`
);
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: some error')).toBeTrue();
expect(returnedError.message.includes('Error Code: server_error_code')).toBeTrue();
expect(returnedError.code).toBe('server_error_code');
});

it('contains a fallback message for authentication errors', async () => {
// don't supply a message during auth failure
mockAuth.and.returnValue(Promise.reject(new Error()));
const mockedAuthError = mockServerErrorResponse(undefined, 'server_error_code');
mockAuth.and.returnValue(
Promise.reject(mockedAuthError)
);

let errorMessage;
let returnedError;
try {
await getIntegrationAccessToken(
{
Expand All @@ -275,12 +333,13 @@ describe('getIntegrationAccessToken', () => {
}
);
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

expect(errorMessage).toBe(
'Error retrieving access token. An unknown authentication error occurred.'
);
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: An unknown authentication error occurred')).toBeTrue();
expect(returnedError.message.includes('Error Code: server_error_code')).toBeTrue();
expect(returnedError.code).toBe('server_error_code');
});
});
});
20 changes: 17 additions & 3 deletions bin/getIntegrationAccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,29 @@ module.exports = async (

return response.access_token;
} catch (e) {
// an unexpected error in jwt-auth
if (!e.code || (verbose && 'request_failed' === e.code)) {
throw e;
}

const errorMessage = e.message || 'An unknown authentication error occurred.';
const isScopeError = errorMessage.toLowerCase().indexOf('invalid_scope') !== -1;
const isScopeError = 'invalid_scope' === e.code;
const hasCheckedFinalScope = i === METASCOPES.length - 1;

// throw immediately if we've encountered any error that isn't a scope error
if (!isScopeError || hasCheckedFinalScope) {
throw new Error(
`Error retrieving access token. ${errorMessage}`
let preAmble = 'Error retrieving your Access Token:';
const message = `Error Message: ${errorMessage}`;
const code = `Error Code: ${e.code}`;
if ('request_failed' === e.code) {
preAmble += ' This is likely an error within jwt-auth or the IMS token exchange service';
}

let preparedError = new Error(
[preAmble, message, code].join('\n')
);
preparedError.code = e.code;
throw preparedError;
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,13 @@ const checkOldProductionEnvironmentVariables = require('./checkOldProductionEnvi
argv
);
} catch (error) {
if (argv.verbose) {
if (argv.verbose || !error.code) {
console.log(chalk.bold.red('--verbose output:'))
throw error;
}

console.log(chalk.bold.red(error.message));
console.log(chalk.bold.red('run in --verbose mode for more info'));
console.log(chalk.bold.red('run in --verbose mode for full stack trace'));
process.exitCode = 1;
}
})();
Loading