-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(bus): implement AxiosCommandDispatcher
- Loading branch information
1 parent
32cc5a0
commit 403d18f
Showing
5 changed files
with
277 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
packages/bus/src/command-dispatchers/AxiosCommandDispatcher.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
77
packages/bus/src/command-dispatchers/AxiosCommandDispatcher.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |