Skip to content

Commit

Permalink
fix: pass all unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Shawn Zhu committed Nov 14, 2023
1 parent 1b58338 commit 7ebacb4
Show file tree
Hide file tree
Showing 12 changed files with 61 additions and 325 deletions.
5 changes: 3 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import inspector from 'inspector';
const testTimeout = inspector.url() ? 1e8 : 10e3;

const config: Config.InitialOptions = {
preset: '@lifeomic/jest-config',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'^.+\\.(j|t)s$': [
'@swc/jest',
{
jsc: { target: 'es2019' },
Expand All @@ -35,6 +34,8 @@ const config: Config.InitialOptions = {
verbose: true,
maxWorkers: '50%',
testTimeout,
// need to transform imported js file using ESM
transformIgnorePatterns: ['/node_modules/(?!(axios)/)'],
};

export default config;
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"author": "LifeOmic <[email protected]>",
"license": "MIT",
"engines": {
"node": ">=14"
"node": ">=16"
},
"files": [
"src/**/*.d.ts",
Expand Down Expand Up @@ -37,12 +37,11 @@
"@types/jest": "^28.1.3",
"@types/lodash": "^4.14.181",
"@types/nearley": "^2.11.2",
"@types/node": "^14.14.25",
"@types/node": "^16.11.7",
"@types/url-parse": "^1.4.8",
"@types/uuid": "^8.3.4",
"aws-sdk-client-mock": "^1.0.0",
"conventional-changelog-conventionalcommits": "^4.0.0",
"coveralls": "^3.1.0",
"eslint": "^8.18.0",
"jest": "^28.1.2",
"jest-mock-extended": "^2.0.6",
Expand Down
6 changes: 3 additions & 3 deletions src/adapters/helpers/lambdaResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TextEncoder } from 'util';
import type { AlphaOptions } from '../../types';
import type { InvocationRequest } from '@aws-sdk/client-lambda';
import type { AxiosResponse } from 'axios';
import { HandlerRequest } from '../../types';
import { type AlphaResponse, HandlerRequest } from '../../types';

export interface Payload {
body: string;
Expand All @@ -27,10 +27,10 @@ export const lambdaResponse = (
config: AlphaOptions,
request: InvocationRequest | HandlerRequest,
payload: Payload,
): AxiosResponse => {
): AlphaResponse => {
const data = payloadToData(config, payload);

const response: AxiosResponse = {
const response: AlphaResponse = {
config,
data,
headers: payload.headers,
Expand Down
2 changes: 1 addition & 1 deletion src/alpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class Alpha extends Axios {
}

get<T = any, R = AlphaResponse<T>, D = any>(url: string, config?: AlphaOptions<D>): Promise<R> {
return super.get(url, config);
return super.get<T, R, D>(url, config);
}
delete<T = any, R = AlphaResponse<T>, D = any>(url: string, config?: AlphaOptions<D>): Promise<R> {
return super.delete(url, config);
Expand Down
14 changes: 8 additions & 6 deletions src/interceptors/aws-v4-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
HttpRequest,
HeaderBag,
} from '@aws-sdk/types';

import buildURL from 'axios/lib/helpers/buildURL';
import transformData from 'axios/lib/core/transformData';
import buildFullPath from 'axios/lib/core/buildFullPath';
// @ts-expect-error internal API is not exported w/ types
import buildURL from 'axios/unsafe/helpers/buildURL.js';
// @ts-expect-error internal API is not exported w/ types
import transformData from 'axios/unsafe/core/transformData.js';
// @ts-expect-error internal API is not exported w/ types
import buildFullPath from 'axios/unsafe/core/buildFullPath.js';

import type { Alpha } from '../alpha';
import type { AlphaInterceptor, AlphaOptions } from '../types';
Expand Down Expand Up @@ -37,7 +39,7 @@ const unsignableHeaders = new Set([
]);

const combineParams = (url: string, { params, paramsSerializer }: AlphaOptions): HttpRequest['query'] => {
const fullUrl = buildURL(url, params, paramsSerializer);
const fullUrl: string = buildURL(url, params, paramsSerializer);
const { query } = parseUrl(fullUrl);
return query;
};
Expand All @@ -55,7 +57,7 @@ const awsV4Signature: AlphaInterceptor = async (config) => {
return config;
}

let fullPath = buildFullPath(config.baseURL, config.url);
let fullPath: string = buildFullPath(config.baseURL, config.url);
if (isLambdaUrl(fullPath)) {
const lambdaUrl = parseLambdaUrl(fullPath) as LambdaUrl;
fullPath = `lambda://${lambdaUrl.name}${lambdaUrl.path}`;
Expand Down
6 changes: 3 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AxiosPromise, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import type { AxiosPromise, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import type { Lambda } from '@aws-sdk/client-lambda';
import type { Context, Handler } from 'aws-lambda';
import { SignatureV4CryptoInit, SignatureV4Init } from '@aws-sdk/signature-v4';
Expand All @@ -14,14 +14,14 @@ type SignatureV4Constructor = SignatureV4Init & SignatureV4CryptoInit;
type SignatureV4Optionals = 'credentials' | 'region' | 'sha256' | 'service';

export interface AlphaResponse<ResponseData = any, ConfigData = any> extends AxiosResponse<ResponseData> {
config: AlphaOptions<ConfigData>;
config: AlphaOptions<ConfigData> & InternalAxiosRequestConfig;
}

export type SignAwsV4Config =
& Omit<SignatureV4Constructor, SignatureV4Optionals>
& Partial<Pick<SignatureV4Constructor, SignatureV4Optionals>>;

export interface AlphaOptions<D = any> extends InternalAxiosRequestConfig<D> {
export interface AlphaOptions<D = any> extends AxiosRequestConfig<D> {
retry?: RetryOptions | boolean;
lambda?: Handler;
context?: Context;
Expand Down
17 changes: 8 additions & 9 deletions test/alpha.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { AxiosHeaders } from 'axios';
import { Handler } from 'aws-lambda';
import { v4 as uuid } from 'uuid';

import { Alpha, AlphaOptions } from '../src';

const handler: jest.MockedFn<Handler> = jest.fn();
const config: AlphaOptions = { headers: new AxiosHeaders() };
const config: AlphaOptions = {};

const response = {
headers: { 'test-header': 'some value' },
Expand Down Expand Up @@ -35,24 +34,24 @@ test('overloaded method signatures', async () => {
const options: AlphaOptions = config;
const data = {};
const alpha = new Alpha(handler);
await expect(alpha.get(path, options)).resolves.toEqual(expect.objectContaining(expected));
await expect(alpha.get(path, options)).resolves.toMatchObject(expected);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path, httpMethod: 'GET' }), expect.any(Object), expect.any(Function));

await expect(alpha.delete(path, options)).resolves.toEqual(expect.objectContaining(expected));
await expect(alpha.delete(path, options)).resolves.toMatchObject(expected);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path, httpMethod: 'DELETE' }), expect.any(Object), expect.any(Function));

await expect(alpha.head(path, options)).resolves.toEqual(expect.objectContaining(expected));
await expect(alpha.head(path, options)).resolves.toMatchObject(expected);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path, httpMethod: 'HEAD' }), expect.any(Object), expect.any(Function));

await expect(alpha.options(path, options)).resolves.toEqual(expect.objectContaining(expected));
await expect(alpha.options(path, options)).resolves.toMatchObject(expected);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path, httpMethod: 'OPTIONS' }), expect.any(Object), expect.any(Function));

await expect(alpha.post(path, data, options)).resolves.toEqual(expect.objectContaining(expected));
await expect(alpha.post(path, data, options)).resolves.toMatchObject(expected);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path, httpMethod: 'POST' }), expect.any(Object), expect.any(Function));

await expect(alpha.put(path, data, options)).resolves.toEqual(expect.objectContaining(expected));
await expect(alpha.put(path, data, options)).resolves.toMatchObject(expected);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path, httpMethod: 'PUT' }), expect.any(Object), expect.any(Function));

await expect(alpha.patch(path, data, options)).resolves.toEqual(expect.objectContaining(expected));
await expect(alpha.patch(path, data, options)).resolves.toMatchObject(expected);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path, httpMethod: 'PATCH' }), expect.any(Object), expect.any(Function));
});
5 changes: 3 additions & 2 deletions test/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AxiosHeaders } from 'axios';
import { Alpha } from '../src';
import nock from 'nock';

Expand All @@ -24,7 +25,7 @@ test('Making a GET request with the http protocol performs a normal HTTP request
expect(response.data).toBe('hello!');
expect(response.status).toBe(200);

expect(response.headers).toEqual({ 'test-header': 'some value' });
expect(response.headers).toEqual(new AxiosHeaders({ 'test-header': 'some value' }));

expect(server.isDone()).toBe(true);
});
Expand All @@ -40,7 +41,7 @@ test('Making a GET request with the https protocol performs a normal HTTPS reque
expect(response.data).toBe('hello!');
expect(response.status).toBe(200);

expect(response.headers).toEqual({ 'test-header': 'some value' });
expect(response.headers).toEqual(new AxiosHeaders({ 'test-header': 'some value' }));

expect(server.isDone()).toBe(true);
});
8 changes: 4 additions & 4 deletions test/interceptors/aws-v4-signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const expectedResponse = {
status: response.statusCode,
statusText: 'OK',
headers: response.headers,
config: expect.anything(),
};

const credentials: Credentials = {
Expand Down Expand Up @@ -56,23 +57,22 @@ test.each<AlphaOptions | undefined>([
} = defaultOptions,
) => {
await expect(alpha.get(path, { signAwsV4, ...options })).resolves
.toEqual(expect.objectContaining(expectedResponse));
.toMatchObject(expectedResponse);

expect(handler).toHaveBeenCalledWith(matchingObj, expect.any(Object), expect.any(Function));
});

test('will get port', async () => {
const alpha = new Alpha(handler, { baseURL: 'https://www.lifeomic.com:80', ...defaultOptions });
await expect(alpha.get(path, { signAwsV4: {}, ...defaultOptions })).resolves
.toEqual(expect.objectContaining(expectedResponse));
.toMatchObject(expectedResponse);

expect(handler).toHaveBeenCalledWith(matchingObj, expect.any(Object), expect.any(Function));
});

test('will not sign requests without config', async () => {
const alpha = new Alpha(handler);
await expect(alpha.get(path)).resolves
.toEqual(expect.objectContaining(expectedResponse));
await expect(alpha.get(path)).resolves.toMatchObject(expectedResponse);

expect(handler).not.toHaveBeenCalledWith(matchingObj, expect.any(Object), expect.any(Function));
});
2 changes: 1 addition & 1 deletion test/lambda-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Handler } from 'aws-lambda';
import { AxiosHeaders } from 'axios';

const response = {
headers: { 'test-header': 'some value' },
headers: new AxiosHeaders({ 'test-header': 'some value' }),
body: 'hello!',
statusCode: 200,
};
Expand Down
28 changes: 10 additions & 18 deletions test/lambda-invocation.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Alpha } from '../src';
import nock from 'nock';
import { Lambda, InvokeCommand, InvokeCommandInput } from '@aws-sdk/client-lambda';
import { AxiosHeaders } from 'axios';
import { mockClient } from 'aws-sdk-client-mock';
import { prepResponse, createResponse } from './utils';
import { AxiosHeaders } from 'axios';

const mockLambda = mockClient(Lambda);
const FakeLambda = jest.fn() as jest.MockedClass<typeof Lambda>;
const defaultOptions = { headers: new AxiosHeaders() };

interface TestContext {
alpha: Alpha;
Expand All @@ -25,7 +24,7 @@ afterAll(() => {

beforeEach(() => {
ctx = {} as TestContext;
ctx.alpha = new Alpha('lambda://test-function', { adapter: undefined, ...defaultOptions });
ctx.alpha = new Alpha('lambda://test-function', { adapter: undefined });
ctx.abort = jest.fn();
FakeLambda.mockReturnValue(new Lambda({}));
});
Expand Down Expand Up @@ -55,11 +54,11 @@ test('Making a GET request with the lambda protocol invokes the lambda function'
},
});

const response = await ctx.alpha.get('/some/path?param1=value1', { params: { param2: 'value2' }, ...defaultOptions });
const response = await ctx.alpha.get('/some/path?param1=value1', { params: { param2: 'value2' } });

expect(response.data).toBe('hello!');
expect(response.status).toBe(200);
expect(response.headers).toEqual({ 'test-header': 'some value' });
expect(response.headers).toEqual(new AxiosHeaders({ 'test-header': 'some value' }));

const cmdInput = getIn();

Expand Down Expand Up @@ -91,7 +90,6 @@ test('Making a GET request with responseType \'arraybuffer\' returns the correct
const response = await ctx.alpha.get<Buffer>('/some/path?param1=value1', {
params: { param2: 'value2' },
responseType: 'arraybuffer',
...defaultOptions,
});

// Assert that the right type is returned
Expand All @@ -114,7 +112,6 @@ test('Making a GET request with responseType \'stream\' throws an unsupported er
const response = ctx.alpha.get('/some/path?param1=value1', {
params: { param2: 'value2' },
responseType: 'stream',
...defaultOptions,
});
await expect(response).rejects.toThrow('Unhandled responseType requested: stream');
});
Expand Down Expand Up @@ -149,7 +146,7 @@ const testLambdaWithQualifier = async (qualifier: string) => {

expect(response.data).toBe('hello!');
expect(response.status).toBe(200);
expect(response.headers).toEqual({ 'test-header': 'some value' });
expect(response.headers).toEqual(new AxiosHeaders({ 'test-header': 'some value' }));

const cmdInput = getIn();
expect(cmdInput).toEqual({
Expand Down Expand Up @@ -198,7 +195,7 @@ test('When a lambda function returns an error code an error is thrown', async ()
expect(error.config.url).toBe('lambda://test-function/some/path');

expect(error.response.status).toBe(400);
expect(error.response.headers).toEqual({});
expect(error.response.headers).toEqual(new AxiosHeaders({}));
expect(error.response.data).toEqual({ error: 'Bad request.' });

expect(error.request.FunctionName).toBe('test-function');
Expand Down Expand Up @@ -226,7 +223,7 @@ test('When status validation is disabled errors are not thrown', async () => {
},
});

const response = await ctx.alpha.get('/some/path', { validateStatus: undefined, ...defaultOptions });
const response = await ctx.alpha.get('/some/path', { validateStatus: undefined });

expect(response.status).toBe(400);
expect(response.data).toBe('error!');
Expand Down Expand Up @@ -439,7 +436,7 @@ test('timeout values are provided to the HTTP client used by the Lambda client',
},
});

await ctx.alpha.get('/some/path', { timeout: 5, Lambda: FakeLambda, ...defaultOptions });
await ctx.alpha.get('/some/path', { timeout: 5, Lambda: FakeLambda });

expect(FakeLambda).toHaveBeenCalledTimes(1);
const { requestHandler } = FakeLambda.mock.calls[0][0];
Expand All @@ -455,7 +452,6 @@ test('A timeout can be configured for the invoked lambda function', async () =>
const promise = ctx.alpha.get('/some/path', {
Lambda: FakeLambda, // lambda will take 1000 ms
timeout: 5, // timeout at 5 ms
...defaultOptions,
});
await expect(promise).rejects.toThrow();
const error = await promise.catch((err) => err);
Expand All @@ -470,11 +466,10 @@ test('A configured timeout does not hinder normal lambda function invocation beh
const response = await ctx.alpha.get('/some/path', {
Lambda: FakeLambda,
timeout: 10,
...defaultOptions,
});
expect(response.data).toBe('hello!');
expect(response.status).toBe(200);
expect(response.headers).toEqual({ 'test-header': 'some value' });
expect(response.headers).toEqual(new AxiosHeaders({ 'test-header': 'some value' }));

// By ending the test 10ms after the timeout, we ensure that the internal setTimeout firing doesn't
// cause any negative side effects, such as attempting to abort after the lambda finished.
Expand All @@ -487,14 +482,13 @@ test('A configured timeout does not hinder normal lambda function invocation beh
});

test('A configured timeout does not eat lambda function invocation errors', async () => {
jest.useFakeTimers();
jest.useFakeTimers({ doNotFake: ['performance'] });
jest.spyOn(global, 'setTimeout');
jest.spyOn(global, 'clearTimeout');
delayedLambda(1, new Error('Other error'));
const promise = ctx.alpha.get('/some/path', {
Lambda: FakeLambda,
timeout: 1000,
...defaultOptions,
});
await expect(promise).rejects.toThrow('Other error');
expect(ctx.abort).not.toHaveBeenCalled();
Expand All @@ -507,15 +501,13 @@ test('lambda function invocation errors are re-thrown', async () => {
delayedLambda(1, new Error('Other error'));
await expect(ctx.alpha.get('/some/path', {
Lambda: FakeLambda,
...defaultOptions,
})).rejects.toThrow('Other error');
});

test('lambdaEndpoint config option is provided to the Lambda client', async () => {
const alpha = new Alpha('lambda://test-function', {
lambdaEndpoint: 'http://test-endpoint',
Lambda: FakeLambda,
...defaultOptions,
});
createResponse(mockLambda, {
StatusCode: 200,
Expand Down
Loading

0 comments on commit 7ebacb4

Please sign in to comment.