Skip to content

Commit

Permalink
fix: fix credential timeout bug & update related mocks
Browse files Browse the repository at this point in the history
  • Loading branch information
hoonoh committed Oct 21, 2019
1 parent 04d0e42 commit 1309500
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 77 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ If no options are applied, it will fetch all recent pricing data from default re

### Credentials

This CLI utility uses AWS-SDK and requires AWS Access & Secret keys. If `~/.aws/credentials` is available it will use it. Otherwise, you will need to supply credentials through CLI options [`--accessKeyId`](#accessKeyId) and [`--secretAccessKey`](#secretAccessKey).
This CLI utility uses AWS-SDK and requires AWS Access & Secret keys. If environment variables pair `AWS_ACCESS_KEY_ID` & `AWS_SECRET_ACCESS_KEY` or `~/.aws/credentials` is available it will use it. Otherwise, you will need to supply credentials through CLI options [`--accessKeyId`](#accessKeyId) and [`--secretAccessKey`](#secretAccessKey).

### Options

Expand Down
30 changes: 24 additions & 6 deletions src/cli.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { spawnSync } from 'child_process';
import mockConsole, { RestoreConsole } from 'jest-mock-console';
import * as nock from 'nock';
import { resolve } from 'path';

import { consoleMockCallJoin, nockEndpoint } from '../test/test-utils';
import {
consoleMockCallJoin,
mockAwsCredentials,
mockAwsCredentialsClear,
mockDefaultRegionEndpoints,
mockDefaultRegionEndpointsClear,
} from '../test/test-utils';
import { main } from './cli';
import { defaultRegions } from './regions';

describe('cli', () => {
describe('test by import', () => {
let restoreConsole: RestoreConsole;

beforeAll(() => {
defaultRegions.forEach(region => nockEndpoint({ region }));
mockDefaultRegionEndpoints();
});

afterAll(() => {
nock.cleanAll();
mockDefaultRegionEndpointsClear();
});

beforeEach(() => {
Expand Down Expand Up @@ -94,8 +98,22 @@ describe('cli', () => {
expect(caughtError).toBeTruthy();
expect(consoleMockCallJoin()).toMatchSnapshot();
});
});

describe('should handle invalid credentials error', () => {
let restoreConsole: RestoreConsole;

beforeAll(() => {
mockAwsCredentials(true);
restoreConsole = mockConsole();
});

afterAll(() => {
mockAwsCredentialsClear();
restoreConsole();
});

it('should handle invalid credentials error', async () => {
it('should throw error', async () => {
let caughtError = false;
try {
await main(['--accessKeyId', 'rand', '--secretAccessKey', 'rand']);
Expand Down
24 changes: 14 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
instanceSizes,
InstanceType,
} from './ec2-types';
import { awsCredentialsCheck, defaults, getGlobalSpotPrices } from './lib';
import { AuthError, awsCredentialsCheck, defaults, getGlobalSpotPrices } from './lib';
import {
allProductDescriptions,
instanceOfProductDescription,
Expand Down Expand Up @@ -189,15 +189,10 @@ export const main = (argvInput?: string[]): Promise<void> =>
}

// test credentials
const awsCredentialValidity = await awsCredentialsCheck({
await awsCredentialsCheck({
accessKeyId,
secretAccessKey,
});
if (!awsCredentialValidity) {
console.log('Invalid AWS credentials provided.');
rej();
return;
}

const productDescriptionsSetArray = Array.from(productDescriptionsSet);
const familyTypeSetArray = Array.from(familyTypeSet);
Expand All @@ -221,9 +216,18 @@ export const main = (argvInput?: string[]): Promise<void> =>

res();
} catch (error) {
/* istanbul ignore next */
console.log('unexpected getGlobalSpotPrices error:', JSON.stringify(error, null, 2));
/* istanbul ignore next */
if (error instanceof AuthError) {
if (error.code === 'UnAuthorized') {
console.log('Invalid AWS credentials provided.');
} else {
// error.reason === 'CredentialsNotFound'
console.log('AWS credentials are not found.');
}
} else {
/* istanbul ignore next */
console.log('unexpected getGlobalSpotPrices error:', JSON.stringify(error, null, 2));
/* istanbul ignore next */
}
rej();
}
},
Expand Down
103 changes: 52 additions & 51 deletions src/lib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import mockConsole, { RestoreConsole } from 'jest-mock-console';
import { filter } from 'lodash';
import * as nock from 'nock';

import { consoleMockCallJoin, nockEndpoint } from '../test/test-utils';
import {
consoleMockCallJoin,
mockAwsCredentials,
mockAwsCredentialsClear,
mockDefaultRegionEndpoints,
mockDefaultRegionEndpointsClear,
} from '../test/test-utils';
import { awsCredentialsCheck, getGlobalSpotPrices } from './lib';
import { defaultRegions, Region } from './regions';
import { Region } from './regions';

describe('lib', () => {
describe('getGlobalSpotPrices', () => {
Expand All @@ -15,13 +21,13 @@ describe('lib', () => {

beforeAll(async () => {
restoreConsole = mockConsole();
defaultRegions.forEach(region => nockEndpoint({ region }));
mockDefaultRegionEndpoints();
results = await getGlobalSpotPrices();
});

afterAll(() => {
restoreConsole();
nock.cleanAll();
mockDefaultRegionEndpointsClear();
});

it('should return expected values', () => {
Expand All @@ -33,9 +39,7 @@ describe('lib', () => {
let results: SpotPrice[];

beforeAll(async () => {
defaultRegions.forEach(region =>
nockEndpoint({ region, maxLength: 5, returnPartialBlankValues: true }),
);
mockDefaultRegionEndpoints({ maxLength: 5, returnPartialBlankValues: true });

results = await getGlobalSpotPrices({
familyTypes: ['c4', 'c5'],
Expand All @@ -48,7 +52,7 @@ describe('lib', () => {
});

afterAll(() => {
nock.cleanAll();
mockDefaultRegionEndpointsClear();
});

it('should return expected values', () => {
Expand All @@ -60,7 +64,7 @@ describe('lib', () => {
let results: SpotPrice[];

beforeAll(async () => {
defaultRegions.forEach(region => nockEndpoint({ region }));
mockDefaultRegionEndpoints();

results = await getGlobalSpotPrices({
familyTypes: ['c4', 'c5'],
Expand All @@ -73,7 +77,7 @@ describe('lib', () => {
});

afterAll(() => {
nock.cleanAll();
mockDefaultRegionEndpointsClear();
});

it('should contain all instance types', () => {
Expand All @@ -95,7 +99,7 @@ describe('lib', () => {
let results: SpotPrice[];

beforeAll(async () => {
defaultRegions.forEach(region => nockEndpoint({ region }));
mockDefaultRegionEndpoints();

results = await getGlobalSpotPrices({
priceMax,
Expand All @@ -104,7 +108,7 @@ describe('lib', () => {
});

afterAll(() => {
nock.cleanAll();
mockDefaultRegionEndpointsClear();
});

it(`should return prices less than ${priceMax}`, () => {
Expand All @@ -120,13 +124,15 @@ describe('lib', () => {

beforeAll(() => {
restoreConsole = mockConsole();
mockAwsCredentials();
nock(`https://ec2.${region}.amazonaws.com`)
.persist()
.post('/')
.reply(400, '');
});
afterAll(() => {
restoreConsole();
mockAwsCredentialsClear();
nock.cleanAll();
});
it('should console log error', async () => {
Expand All @@ -142,47 +148,42 @@ describe('lib', () => {
nock.cleanAll();
});

it('should return false', async () => {
nock('https://sts.amazonaws.com')
.persist()
.post('/')
.reply(
403,
`<ErrorResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<Error>
<Type>Sender</Type>
<Code>MissingAuthenticationToken</Code>
<Message>Request is missing Authentication Token</Message>
</Error>
<RequestId>4fc0d3ee-efef-11e9-9282-3b7bffe54a9b</RequestId>
</ErrorResponse>`,
);
const results = await awsCredentialsCheck();
expect(results).toBeFalsy();
describe('should throw error', () => {
beforeEach(() => {
mockAwsCredentials(true);
});

afterEach(() => {
mockAwsCredentialsClear();
});

it('should throw error', async () => {
let threwError = false;
try {
await awsCredentialsCheck();
} catch (error) {
threwError = true;
}
expect(threwError).toBeTruthy();
});
});

it('should return true', async () => {
nock('https://sts.amazonaws.com')
.persist()
.post('/')
.reply(
200,
`<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::123456789012:user/Alice</Arn>
<UserId>EXAMPLE</UserId>
<Account>123456789012</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>01234567-89ab-cdef-0123-456789abcdef</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>`,
);
const results = await awsCredentialsCheck({
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
});
expect(results).toBeTruthy();
describe('should not throw error', () => {
beforeEach(() => {
mockAwsCredentials();
});
afterEach(() => {
mockAwsCredentialsClear();
});
it('should not throw error', async () => {
let threwError = false;
try {
await awsCredentialsCheck();
} catch (error) {
threwError = true;
}
expect(threwError).toBeFalsy();
});
});
});
});
33 changes: 26 additions & 7 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EC2, STS } from 'aws-sdk';
import { config, EC2, STS } from 'aws-sdk';
import { find, findIndex } from 'lodash';
import * as ora from 'ora';
import { table } from 'table';
Expand Down Expand Up @@ -47,9 +47,9 @@ class Ec2SpotPriceError extends Error {
Object.setPrototypeOf(this, Ec2SpotPriceError.prototype);
}

region: string;
readonly region: string;

code: string;
readonly code: string;
}

const getEc2SpotPrice = async (options: {
Expand Down Expand Up @@ -269,21 +269,40 @@ export const getGlobalSpotPrices = async (options?: {
return rtn;
};

type AuthErrorCode = 'CredentialsNotFound' | 'UnAuthorized';

export class AuthError extends Error {
constructor(message: string, code: AuthErrorCode) {
super(message);
this.code = code;
Object.setPrototypeOf(this, AuthError.prototype);
}

readonly code: AuthErrorCode;
}

export const awsCredentialsCheck = async (options?: {
accessKeyId?: string;
secretAccessKey?: string;
}): Promise<boolean> => {
}): Promise<void> => {
const { accessKeyId, secretAccessKey } = options || {};

let isValid = true;
if (
!accessKeyId &&
!secretAccessKey &&
!config.credentials &&
!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)
) {
throw new AuthError('AWS credentials unavailable.', 'CredentialsNotFound');
}

try {
const sts = new STS({
accessKeyId,
secretAccessKey,
});
await sts.getCallerIdentity().promise();
} catch (error) {
isValid = false;
throw new AuthError(error.message, 'UnAuthorized');
}
return isValid;
};
Loading

0 comments on commit 1309500

Please sign in to comment.