Skip to content

Commit

Permalink
feat(bus): implement AxiosCommandDispatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
RomanReznichenko committed Apr 11, 2022
1 parent 32cc5a0 commit 403d18f
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 1 deletion.
58 changes: 57 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"dependencies": {
"amqp-connection-manager": "^4.1.1",
"amqplib": "^0.8.0",
"axios": "^0.26.1",
"axios-rate-limit": "^1.3.0",
"reflect-metadata": "^0.1.13",
"tslib": "~2.3.1",
"tsyringe": "^4.6.0",
Expand Down
140 changes: 140 additions & 0 deletions packages/bus/src/command-dispatchers/AxiosCommandDispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { HttpCommand } from './HttpCommand';
import { AxiosCommandDispatcher } from './AxiosCommandDispatcher';
import { HttpCommandDispatcherConfig } from './HttpCommandDispatcherConfig';
import { RateLimitedAxiosInstance } from 'axios-rate-limit';
import {
anything,
deepEqual,
instance,
mock,
reset,
verify,
when
} from 'ts-mockito';
import { AxiosResponse, Method } from 'axios';

class ConcreteCommand extends HttpCommand<string, string | undefined> {
constructor(
payload: string,
url: string,
method: Method,
expectReply?: boolean,
ttl?: number
) {
super(payload, url, method, expectReply, ttl);
}
}

describe('AxiosCommandDispatcher', () => {
const mockedAxiosRateLimit = jest.fn();
const mockedRateLimitedAxiosInstance = mock<RateLimitedAxiosInstance>();
const options: HttpCommandDispatcherConfig = {
url: 'https://example.com'
};

let axiosDispatcher: AxiosCommandDispatcher;

beforeEach(() => {
jest.mock('axios-rate-limit', () =>
mockedAxiosRateLimit.mockImplementation(() =>
instance(mockedRateLimitedAxiosInstance)
)
);

axiosDispatcher = new AxiosCommandDispatcher(options);
});

afterEach(() => {
reset<RateLimitedAxiosInstance>(mockedRateLimitedAxiosInstance);

jest.resetModules();
jest.resetAllMocks();
});

describe('init', () => {
it('should create axios', async () => {
await axiosDispatcher.init?.();

expect(mockedAxiosRateLimit).toHaveBeenCalledTimes(1);
});
});

describe('execute', () => {
it('should throw an error if client is not initialized yet', async () => {
const command = new ConcreteCommand('test', '/api/test', 'POST');

const result = axiosDispatcher.execute(command);

await expect(result).rejects.toThrow(
'established a connection with host'
);
});

it('should send message', async () => {
const command = new ConcreteCommand('test', '/api/test', 'POST', false);
when(mockedRateLimitedAxiosInstance.request(anything())).thenResolve();
await axiosDispatcher.init?.();

await axiosDispatcher.execute(command);

verify(
mockedRateLimitedAxiosInstance.request(
deepEqual({
url: command.url,
method: command.method,
data: command.payload,
timeout: command.ttl
})
)
).once();
});

it('should send message and get reply', async () => {
const command = new ConcreteCommand('test', '/api/test', 'POST', true);
const response: AxiosResponse = {
config: {},
headers: {},
status: 200,
statusText: 'OK',
data: 'result'
};
when(mockedRateLimitedAxiosInstance.request(anything())).thenResolve(
response
);
await axiosDispatcher.init?.();

const result = await axiosDispatcher.execute(command);

expect(result).toEqual(response.data);
verify(
mockedRateLimitedAxiosInstance.request(
deepEqual({
url: command.url,
method: command.method,
data: command.payload,
timeout: command.ttl
})
)
).once();
});

it('should throw a error if no response', async () => {
const command = new ConcreteCommand('test', '/api/test', 'POST', true, 1);
await axiosDispatcher.init?.();

const result = axiosDispatcher.execute(command);
verify(
mockedRateLimitedAxiosInstance.request(
deepEqual({
url: command.url,
method: command.method,
data: command.payload,
timeout: command.ttl
})
)
).once();

await expect(result).rejects.toThrow();
});
});
});
77 changes: 77 additions & 0 deletions packages/bus/src/command-dispatchers/AxiosCommandDispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { HttpCommandDispatcher } from './HttpCommandDispatcher';
import { HttpCommandDispatcherConfig } from './HttpCommandDispatcherConfig';
import { HttpCommand } from './HttpCommand';
import { IllegalOperation } from '@secbox/core';
import { inject } from 'tsyringe';
import axios, { AxiosRequestHeaders, AxiosRequestConfig } from 'axios';
import { RateLimitedAxiosInstance } from 'axios-rate-limit';

export class AxiosCommandDispatcher implements HttpCommandDispatcher {
private client?: RateLimitedAxiosInstance;
constructor(
@inject(HttpCommandDispatcherConfig)
private readonly options: HttpCommandDispatcherConfig
) {}

public async init(): Promise<void> {
const axiosRateLimit = (await import('axios-rate-limit')).default;
const { axiosLimitOptions } = this.options;
const axiosOptions = this.getAxiosOptions(this.options);

this.client = axiosRateLimit(
axios.create(axiosOptions),
axiosLimitOptions ?? {}
);
}

public async execute<T, R>(
command: HttpCommand<T, R>
): Promise<R | undefined> {
if (!this.client) {
throw new IllegalOperation(this);
}

const { url, method, ttl, payload, expectReply } = command;

const response = this.client.request({
url,
method,
data: payload,
timeout: ttl
});

return expectReply ? (await response).data : undefined;
}

private getAxiosOptions(
options: HttpCommandDispatcherConfig
): AxiosRequestConfig {
const headers = this.getHeaders(options);

return {
baseURL: options.url,
headers
};
}

private getHeaders(
options: HttpCommandDispatcherConfig
): AxiosRequestHeaders {
let headers: AxiosRequestHeaders = {};
if (options.credentials) {
headers = {
...headers,
authorization: `Basic ${this.getToken(
options.credentials.username,
options.credentials?.password
)}`
};
}

return headers;
}

private getToken(user: string, apiKey: string): string {
return Buffer.from(`${user}:${apiKey}`).toString('base64');
}
}
1 change: 1 addition & 0 deletions packages/bus/src/command-dispatchers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './HttpCommand';
export * from './HttpCommandDispatcher';
export * from './AxiosCommandDispatcher';
export * from './HttpCommandDispatcherConfig';

0 comments on commit 403d18f

Please sign in to comment.