@squiz/optimization-utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/cloudflare/CloudflareKVHttpService.d.ts +11 -0
  2. package/dist/cloudflare/CloudflareKVHttpService.js +19 -0
  3. package/dist/cloudflare/CloudflareKVHttpService.js.map +1 -0
  4. package/dist/cloudflare/ImplCloudflareKVHttpService.d.ts +22 -0
  5. package/dist/cloudflare/ImplCloudflareKVHttpService.js +96 -0
  6. package/dist/cloudflare/ImplCloudflareKVHttpService.js.map +1 -0
  7. package/dist/config/ConfigurationLoader.d.ts +23 -0
  8. package/dist/config/ConfigurationLoader.js +99 -0
  9. package/dist/config/ConfigurationLoader.js.map +1 -0
  10. package/dist/date/DateManipulator.d.ts +6 -0
  11. package/dist/date/DateManipulator.js +21 -0
  12. package/dist/date/DateManipulator.js.map +1 -0
  13. package/dist/event/AggregateRoot.d.ts +4 -0
  14. package/dist/event/AggregateRoot.js +3 -0
  15. package/dist/event/AggregateRoot.js.map +1 -0
  16. package/dist/event/DomainEvent.d.ts +13 -0
  17. package/dist/event/DomainEvent.js +28 -0
  18. package/dist/event/DomainEvent.js.map +1 -0
  19. package/dist/event/DynamoDBEventMapper.d.ts +36 -0
  20. package/dist/event/DynamoDBEventMapper.js +55 -0
  21. package/dist/event/DynamoDBEventMapper.js.map +1 -0
  22. package/dist/event/EventHandler.d.ts +14 -0
  23. package/dist/event/EventHandler.js +43 -0
  24. package/dist/event/EventHandler.js.map +1 -0
  25. package/dist/exception/DomainException.d.ts +18 -0
  26. package/dist/exception/DomainException.js +41 -0
  27. package/dist/exception/DomainException.js.map +1 -0
  28. package/dist/httpClient/FetchHttpClient.d.ts +7 -0
  29. package/dist/httpClient/FetchHttpClient.js +86 -0
  30. package/dist/httpClient/FetchHttpClient.js.map +1 -0
  31. package/dist/httpClient/HttpClient.d.ts +25 -0
  32. package/dist/httpClient/HttpClient.js +45 -0
  33. package/dist/httpClient/HttpClient.js.map +1 -0
  34. package/dist/httpClient/HttpRequestBuilder.d.ts +22 -0
  35. package/dist/httpClient/HttpRequestBuilder.js +126 -0
  36. package/dist/httpClient/HttpRequestBuilder.js.map +1 -0
  37. package/dist/logger/Logger.d.ts +10 -0
  38. package/dist/logger/Logger.js +30 -0
  39. package/dist/logger/Logger.js.map +1 -0
  40. package/dist/logger/LoggerMessage.d.ts +27 -0
  41. package/dist/logger/LoggerMessage.js +94 -0
  42. package/dist/logger/LoggerMessage.js.map +1 -0
  43. package/dist/logger/RemoteLogger.d.ts +30 -0
  44. package/dist/logger/RemoteLogger.js +35 -0
  45. package/dist/logger/RemoteLogger.js.map +1 -0
  46. package/dist/logger/SquizRemoteLogger.d.ts +53 -0
  47. package/dist/logger/SquizRemoteLogger.js +128 -0
  48. package/dist/logger/SquizRemoteLogger.js.map +1 -0
  49. package/dist/package.d.ts +23 -0
  50. package/dist/package.js +40 -0
  51. package/dist/package.js.map +1 -0
  52. package/dist/scheduler/EventBridgeScheduler.d.ts +21 -0
  53. package/dist/scheduler/EventBridgeScheduler.js +131 -0
  54. package/dist/scheduler/EventBridgeScheduler.js.map +1 -0
  55. package/dist/scheduler/Scheduler.d.ts +21 -0
  56. package/dist/scheduler/Scheduler.js +17 -0
  57. package/dist/scheduler/Scheduler.js.map +1 -0
  58. package/dist/testing/mock.d.ts +12 -0
  59. package/dist/testing/mock.js +51 -0
  60. package/dist/testing/mock.js.map +1 -0
  61. package/dist/typesUtils/DynamoDB.d.ts +4 -0
  62. package/dist/typesUtils/DynamoDB.js +3 -0
  63. package/dist/typesUtils/DynamoDB.js.map +1 -0
  64. package/dist/typesUtils/utilities.d.ts +9 -0
  65. package/dist/typesUtils/utilities.js +3 -0
  66. package/dist/typesUtils/utilities.js.map +1 -0
  67. package/dist/validation/handleValidation.d.ts +2 -0
  68. package/dist/validation/handleValidation.js +11 -0
  69. package/dist/validation/handleValidation.js.map +1 -0
  70. package/dist/valueObject/TenantId.d.ts +10 -0
  71. package/dist/valueObject/TenantId.js +23 -0
  72. package/dist/valueObject/TenantId.js.map +1 -0
  73. package/package.json +26 -0
  74. package/src/cloudflare/CloudflareKVHttpService.ts +20 -0
  75. package/src/cloudflare/ImplCloudflareKVHttpService.ts +128 -0
  76. package/src/cloudflare/__tests__/ImplCloudflareKVHttpService.spec.ts +178 -0
  77. package/src/config/ConfigurationLoader.ts +72 -0
  78. package/src/config/__tests__/ConfigurationLoader.spec.ts +62 -0
  79. package/src/date/DateManipulator.ts +29 -0
  80. package/src/date/__tests__/DateManipulator.spec.ts +64 -0
  81. package/src/event/AggregateRoot.ts +5 -0
  82. package/src/event/DomainEvent.ts +52 -0
  83. package/src/event/DynamoDBEventMapper.ts +72 -0
  84. package/src/event/EventHandler.ts +57 -0
  85. package/src/event/__tests__/DynamoDBEventMapper.spec.ts +113 -0
  86. package/src/exception/DomainException.ts +34 -0
  87. package/src/httpClient/FetchHttpClient.ts +92 -0
  88. package/src/httpClient/HttpClient.ts +46 -0
  89. package/src/httpClient/HttpRequestBuilder.ts +120 -0
  90. package/src/httpClient/__tests__/FetchHttpClient.spec.ts +146 -0
  91. package/src/httpClient/__tests__/HttpClient.spec.ts +52 -0
  92. package/src/httpClient/__tests__/httpRequestBuilder.spec.ts +75 -0
  93. package/src/logger/Logger.ts +40 -0
  94. package/src/logger/LoggerMessage.ts +151 -0
  95. package/src/logger/RemoteLogger.ts +32 -0
  96. package/src/logger/SquizRemoteLogger.ts +157 -0
  97. package/src/logger/__tests__/LoggerMessage.spec.ts +133 -0
  98. package/src/logger/__tests__/SquizRemoteLogger.spec.ts +185 -0
  99. package/src/package.ts +23 -0
  100. package/src/scheduler/EventBridgeScheduler.ts +177 -0
  101. package/src/scheduler/Scheduler.ts +32 -0
  102. package/src/scheduler/__tests__/EventBridgeScheduler.spec.ts +310 -0
  103. package/src/testing/mock.ts +62 -0
  104. package/src/typesUtils/DynamoDB.ts +17 -0
  105. package/src/typesUtils/utilities.ts +11 -0
  106. package/src/validation/handleValidation.ts +13 -0
  107. package/src/valueObject/TenantId.ts +27 -0
  108. package/tsconfig.json +13 -0
@@ -0,0 +1,57 @@
1
+ import { injectable } from 'inversify';
2
+ import { DomainEvent } from './DomainEvent';
3
+
4
+ @injectable()
5
+ export abstract class EventHandler<TEvent extends DomainEvent = DomainEvent> {
6
+ abstract execute(event: TEvent): Promise<void>;
7
+ }
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ type Constructor = new (...args: Array<any>) => any;
11
+ type EventConstructor = Constructor;
12
+ type EventHandlerConstructor = Constructor;
13
+ type EventHandlerMetadata = Array<{
14
+ eventHandler: EventHandlerConstructor;
15
+ event: EventConstructor;
16
+ }>;
17
+
18
+ const EVENT_HANDLER_SYMBOL = Symbol();
19
+
20
+ export function getEventHandlerMetadata(): EventHandlerMetadata {
21
+ return Reflect.getMetadata(EVENT_HANDLER_SYMBOL, Object) ?? [];
22
+ }
23
+
24
+ function setEventHandlerMetadata({
25
+ event,
26
+ eventHandler,
27
+ }: {
28
+ eventHandler: EventHandlerConstructor;
29
+ event: EventConstructor;
30
+ }): void {
31
+ const metadata = getEventHandlerMetadata();
32
+ const handlerExists = metadata.find(
33
+ (m) => m.eventHandler.name === eventHandler.name,
34
+ );
35
+
36
+ if (handlerExists) {
37
+ throw new Error(
38
+ `The EventHandler has been used two times: ${eventHandler.name}. The EventHandler has to have uniq name`,
39
+ );
40
+ }
41
+
42
+ const newMetadata: EventHandlerMetadata = [
43
+ ...getEventHandlerMetadata(),
44
+ { eventHandler, event },
45
+ ];
46
+
47
+ Reflect.defineMetadata(EVENT_HANDLER_SYMBOL, newMetadata, Object);
48
+ }
49
+
50
+ export function AsEventHandler(event: EventConstructor): ClassDecorator {
51
+ return (target) => {
52
+ setEventHandlerMetadata({
53
+ event,
54
+ eventHandler: target as unknown as EventHandlerConstructor,
55
+ });
56
+ };
57
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ EventDynamoDBModel,
3
+ eventToDynamoDB,
4
+ toEventFromPlain,
5
+ toEventFromDynamoDb,
6
+ } from '../DynamoDBEventMapper';
7
+ import { faker } from '@faker-js/faker';
8
+ import { marshall } from '@aws-sdk/util-dynamodb';
9
+ import { TenantIdValue } from '../../valueObject/TenantId';
10
+ import { AsDomainEvent, DomainEvent } from '../DomainEvent';
11
+
12
+ type ExampleEventDetail = {
13
+ experimentId: string;
14
+ tenantId: TenantIdValue;
15
+ };
16
+
17
+ @AsDomainEvent()
18
+ class ExampleEvent implements DomainEvent<{}> {
19
+ readonly detail!: ExampleEventDetail;
20
+ readonly eventId!: string;
21
+ readonly name!: string;
22
+ readonly time!: Date;
23
+ readonly version!: string;
24
+
25
+ constructor(data: Omit<ExampleEvent, 'name'>) {
26
+ this.detail = data.detail;
27
+ this.eventId = data.eventId;
28
+ this.name = ExampleEvent.name;
29
+ this.time = data.time;
30
+ this.version = data.version;
31
+ }
32
+ }
33
+
34
+ const createPlainExampleEvent: () => {
35
+ eventId: string;
36
+ name: string;
37
+ detail: ExampleEventDetail;
38
+ time: string;
39
+ version: string;
40
+ } = () => {
41
+ return {
42
+ detail: {
43
+ experimentId: faker.string.uuid(),
44
+ tenantId: faker.string.uuid(),
45
+ },
46
+ eventId: faker.string.uuid(),
47
+ time: faker.date.past().toISOString(),
48
+ version: faker.number.int().toString(),
49
+ name: ExampleEvent.name,
50
+ ttl: faker.date.past().getTime() / 1000,
51
+ };
52
+ };
53
+
54
+ describe('toEventEntityFromDynamoDb', () => {
55
+ it('should map dynamo item to event instance', () => {
56
+ const event = createPlainExampleEvent();
57
+
58
+ const result = toEventFromDynamoDb<ExampleEvent>(marshall(event));
59
+
60
+ expect(result).toEqual(
61
+ new ExampleEvent({
62
+ detail: event.detail,
63
+ eventId: event.eventId,
64
+ time: new Date(event.time),
65
+ version: event.version,
66
+ }),
67
+ );
68
+ });
69
+ });
70
+
71
+ describe('toEventFromPlain', () => {
72
+ const event = createPlainExampleEvent();
73
+
74
+ it('should map the plain object to event instance', () => {
75
+ const result = toEventFromPlain<ExampleEvent>(event);
76
+
77
+ expect(result).toEqual(
78
+ new ExampleEvent({
79
+ detail: event.detail,
80
+ eventId: event.eventId,
81
+ time: new Date(event.time),
82
+ version: event.version,
83
+ }),
84
+ );
85
+ });
86
+ });
87
+
88
+ describe('toDynamoDB', () => {
89
+ it('should map event entity to dynamo item', () => {
90
+ const event = new ExampleEvent({
91
+ detail: {
92
+ experimentId: faker.string.uuid(),
93
+ tenantId: faker.string.uuid(),
94
+ },
95
+ eventId: faker.string.uuid(),
96
+ time: new Date('2020-01-01T00:00:00.000Z'),
97
+ version: faker.number.int().toString(),
98
+ });
99
+
100
+ const result = eventToDynamoDB(event);
101
+
102
+ expect(result).toEqual(
103
+ marshall({
104
+ detail: event.detail,
105
+ eventId: event.eventId,
106
+ time: event.time.toISOString(),
107
+ version: event.version,
108
+ name: 'ExampleEvent',
109
+ ttl: new Date('2020-01-31T00:00:00.000Z').getTime() / 1000,
110
+ } as EventDynamoDBModel),
111
+ );
112
+ });
113
+ });
@@ -0,0 +1,34 @@
1
+ export abstract class DomainException extends Error {
2
+ protected constructor(message: string) {
3
+ super(message);
4
+
5
+ Error.captureStackTrace(this, this.constructor);
6
+ }
7
+ }
8
+
9
+ export class ValidationException extends DomainException {
10
+ constructor(
11
+ readonly message = 'Validation failed',
12
+ readonly details?: unknown,
13
+ ) {
14
+ super(message);
15
+ }
16
+ }
17
+
18
+ export class UnprocessableEntityException extends DomainException {
19
+ constructor(
20
+ readonly message = 'Unprocessable entity',
21
+ readonly details?: unknown,
22
+ ) {
23
+ super(message);
24
+ }
25
+ }
26
+
27
+ export class NotFoundException extends DomainException {
28
+ constructor(
29
+ readonly message = 'Not found',
30
+ readonly details?: unknown,
31
+ ) {
32
+ super(message);
33
+ }
34
+ }
@@ -0,0 +1,92 @@
1
+ import { inject, injectable } from 'inversify';
2
+ import { Logger } from '../logger/Logger';
3
+ import { createLog } from '../logger/LoggerMessage';
4
+ import fetch from 'node-fetch-commonjs';
5
+ import {
6
+ HttpClient,
7
+ HttpRequestOptions,
8
+ HttpResponse,
9
+ } from '../httpClient/HttpClient';
10
+ import { captureHTTPsGlobal } from 'aws-xray-sdk';
11
+ import http from 'http';
12
+ import https from 'https';
13
+
14
+ captureHTTPsGlobal(https, true);
15
+ captureHTTPsGlobal(http, true);
16
+
17
+ @injectable()
18
+ export class FetchHttpClient implements HttpClient {
19
+ constructor(
20
+ @inject(Logger)
21
+ private readonly logger: Logger,
22
+ ) {}
23
+
24
+ async sendRequest({
25
+ headers = {},
26
+ url,
27
+ method,
28
+ body,
29
+ handleHttpErrorStatusCodes,
30
+ }: HttpRequestOptions): Promise<HttpResponse> {
31
+ const logMessage = createLog()
32
+ .attachMetadata({ url: url.toString() })
33
+ .attachMetadata({ method })
34
+ .create(this);
35
+ const applicationJsonHeader = 'application/json';
36
+ const isApplicationJson = headers['Content-Type'] === applicationJsonHeader;
37
+ const requestBody = body && isApplicationJson ? JSON.stringify(body) : body;
38
+
39
+ this.logger.debug(...logMessage('started sending http request'));
40
+ const response = await fetch(url, {
41
+ headers,
42
+ method,
43
+ ...(body && { body: requestBody }),
44
+ });
45
+
46
+ this.logger.debug(...logMessage('finished sending http request'));
47
+
48
+ const responseContentType = response.headers.get('Content-Type');
49
+ const isJsonResponse = responseContentType?.includes(applicationJsonHeader);
50
+
51
+ this.logger.debug(
52
+ ...logMessage(
53
+ `the response ` +
54
+ `${isJsonResponse ? 'contains' : 'does not contain'} ` +
55
+ `the application/json header`,
56
+ ),
57
+ );
58
+ this.logger.debug(...logMessage('available headers in the next log:'));
59
+ this.logger.debug(...logMessage(response.headers));
60
+ const responseBodyRawText = await response.text();
61
+ const responseBody =
62
+ isJsonResponse && responseBodyRawText
63
+ ? JSON.parse(responseBodyRawText)
64
+ : responseBodyRawText;
65
+ const httpResponse = new HttpResponse({
66
+ statusCode: response.status,
67
+ body: responseBody,
68
+ });
69
+
70
+ const shouldBeHandled = handleHttpErrorStatusCodes?.includes(
71
+ response.status,
72
+ );
73
+
74
+ if (shouldBeHandled) {
75
+ this.logger.debug(...logMessage('handled NotFound as an empty response'));
76
+ return httpResponse;
77
+ }
78
+
79
+ if (!response.ok) {
80
+ this.logger.error(
81
+ ...logMessage(`http response status: ${response.status}`),
82
+ );
83
+ this.logger.debug(...logMessage(`http response body in the next log`));
84
+ this.logger.debug(...logMessage({ responseBodyRawText }));
85
+ throw new Error(`Error occurs calling: ${method} ${url}`, {
86
+ cause: responseBody,
87
+ });
88
+ }
89
+
90
+ return httpResponse;
91
+ }
92
+ }
@@ -0,0 +1,46 @@
1
+ import { injectable } from 'inversify';
2
+ import { captureHTTPsGlobal } from 'aws-xray-sdk';
3
+ import { ClassFields } from './../typesUtils/utilities';
4
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
5
+ captureHTTPsGlobal(require('https'));
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ captureHTTPsGlobal(require('http'));
8
+
9
+ export type HttpRequestOptions = {
10
+ url: URL | string;
11
+ method: HttpMethod;
12
+ body?: string | Record<string, string> | Array<unknown>;
13
+ headers?: Record<string, string>;
14
+ handleHttpErrorStatusCodes?: Array<number>;
15
+ };
16
+
17
+ export enum HttpMethod {
18
+ GET = 'GET',
19
+ POST = 'POST',
20
+ DELETE = 'DELETE',
21
+ PUT = 'PUT',
22
+ PATCH = 'PATCH',
23
+ }
24
+
25
+ export class HttpResponse {
26
+ readonly statusCode: number;
27
+ readonly body: unknown;
28
+
29
+ constructor(opts: ClassFields<HttpResponse>) {
30
+ this.statusCode = opts.statusCode;
31
+ this.body = opts.body;
32
+ }
33
+
34
+ hasHttpStatus(statusCode: number): boolean {
35
+ return this.statusCode === statusCode;
36
+ }
37
+
38
+ isNotFound(): boolean {
39
+ return this.hasHttpStatus(404);
40
+ }
41
+ }
42
+
43
+ @injectable()
44
+ export abstract class HttpClient {
45
+ abstract sendRequest(opts: HttpRequestOptions): Promise<HttpResponse>;
46
+ }
@@ -0,0 +1,120 @@
1
+ import { injectable } from 'inversify';
2
+ import { HttpClient, HttpRequestOptions, HttpResponse } from './HttpClient';
3
+
4
+ @injectable()
5
+ export class HttpRequestBuilderFactory {
6
+ constructor(private readonly httpClient: HttpClient) {}
7
+
8
+ create(): Pick<HttpRequestBuilder, 'url'> {
9
+ return new HttpRequestBuilder({}, this.httpClient);
10
+ }
11
+ }
12
+
13
+ export class HttpRequestBuilder {
14
+ constructor(
15
+ private opts: Partial<HttpRequestOptions>,
16
+ private readonly httpClient: HttpClient,
17
+ ) {}
18
+
19
+ url(url: HttpRequestOptions['url']): Pick<HttpRequestBuilder, 'method'> {
20
+ return this.setOptions({
21
+ ...this.opts,
22
+ url,
23
+ });
24
+ }
25
+
26
+ method(method: HttpRequestOptions['method']): HttpRequestBuilder {
27
+ return this.setOptions({
28
+ ...this.opts,
29
+ method,
30
+ });
31
+ }
32
+
33
+ headers(headers: HttpRequestOptions['headers']): HttpRequestBuilder {
34
+ return this.setOptions({
35
+ ...this.opts,
36
+ headers: {
37
+ ...this.opts.headers,
38
+ ...headers,
39
+ },
40
+ });
41
+ }
42
+
43
+ body(body?: HttpRequestOptions['body']): HttpRequestBuilder {
44
+ return this.setOptions({
45
+ ...this.opts,
46
+ body,
47
+ });
48
+ }
49
+
50
+ authorizationByBearer(token: string): HttpRequestBuilder {
51
+ return this.setOptions({
52
+ ...this.opts,
53
+ headers: {
54
+ ...this.opts.headers,
55
+ Authorization: `Bearer ${token}`,
56
+ },
57
+ });
58
+ }
59
+
60
+ authorizationByXApiKey(apiKey: string): HttpRequestBuilder {
61
+ return this.setOptions({
62
+ ...this.opts,
63
+ headers: {
64
+ ...this.opts.headers,
65
+ 'x-api-key': apiKey,
66
+ },
67
+ });
68
+ }
69
+
70
+ applicationJson(): HttpRequestBuilder {
71
+ return this.setOptions({
72
+ ...this.opts,
73
+ headers: {
74
+ ...this.opts.headers,
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ });
78
+ }
79
+
80
+ handleNotFound(): HttpRequestBuilder {
81
+ return this.handleErrorStatusCode(404);
82
+ }
83
+
84
+ handleErrorStatusCode(errorStatusCode: number): HttpRequestBuilder {
85
+ return this.setOptions({
86
+ ...this.opts,
87
+ handleHttpErrorStatusCodes: [
88
+ ...new Set([
89
+ ...(this.opts.handleHttpErrorStatusCodes ?? []),
90
+ errorStatusCode,
91
+ ]),
92
+ ],
93
+ });
94
+ }
95
+
96
+ private setOptions(opts: Partial<HttpRequestOptions>): HttpRequestBuilder {
97
+ this.opts = opts;
98
+ return this;
99
+ }
100
+
101
+ sendRequest(): Promise<HttpResponse> {
102
+ if (!this.opts.url) {
103
+ throw new Error('The url is not defined');
104
+ }
105
+
106
+ if (!this.opts.method) {
107
+ throw new Error('The method is not defined');
108
+ }
109
+
110
+ // this is only for typescript information, without extra variable typescript thinks that
111
+ // the url and method are undefined
112
+ const options: HttpRequestOptions = {
113
+ url: this.opts.url,
114
+ method: this.opts.method,
115
+ ...this.opts,
116
+ };
117
+
118
+ return this.httpClient.sendRequest(options);
119
+ }
120
+ }
@@ -0,0 +1,146 @@
1
+ import {
2
+ HttpClient,
3
+ HttpMethod,
4
+ HttpRequestOptions,
5
+ HttpResponse,
6
+ } from '../HttpClient';
7
+ import { Container } from 'inversify';
8
+ import { FetchHttpClient } from '../FetchHttpClient';
9
+ import fetch from 'node-fetch-commonjs';
10
+ import { faker } from '@faker-js/faker';
11
+ import Mock = jest.Mock;
12
+ import { Logger } from '../../logger/Logger';
13
+ import {
14
+ createLoggerMock,
15
+ createResponse,
16
+ FetchMock,
17
+ } from '../../testing/mock';
18
+
19
+ jest.mock('node-fetch-commonjs');
20
+
21
+ describe('FetchHttpClient', () => {
22
+ let httpClient: HttpClient;
23
+ let mockFetch: FetchMock;
24
+ let params: HttpRequestOptions;
25
+
26
+ beforeEach(() => {
27
+ const container = new Container();
28
+
29
+ container.bind(Logger).toConstantValue(createLoggerMock());
30
+ container.bind(HttpClient).to(FetchHttpClient);
31
+ mockFetch = fetch as Mock;
32
+ mockFetch.mockReset();
33
+ httpClient = container.get(HttpClient);
34
+
35
+ params = {
36
+ url: faker.internet.url(),
37
+ body: faker.word.words(),
38
+ headers: {
39
+ 'example-header': faker.word.words(),
40
+ },
41
+ method: faker.helpers.arrayElement(Object.values(HttpMethod)),
42
+ };
43
+ });
44
+
45
+ describe('passing params to fetch', () => {
46
+ it('should call the fetch with passed parameters', async () => {
47
+ mockFetch.mockResolvedValueOnce(createResponse(200, {}));
48
+
49
+ await httpClient.sendRequest(params);
50
+
51
+ expect(mockFetch).toHaveBeenCalledWith(params.url, {
52
+ body: params.body,
53
+ method: params.method,
54
+ headers: params.headers,
55
+ });
56
+ });
57
+
58
+ it('should parse request body to JSON if passed Content-Type: application/json', async () => {
59
+ mockFetch.mockResolvedValueOnce(createResponse(200, {}));
60
+ const body = { example: faker.word.words() };
61
+
62
+ await httpClient.sendRequest({
63
+ ...params,
64
+ body,
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ },
68
+ });
69
+
70
+ expect(mockFetch).toHaveBeenCalledWith(params.url, {
71
+ body: JSON.stringify(body),
72
+ method: params.method,
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ },
76
+ });
77
+ });
78
+ });
79
+
80
+ describe('handling response', () => {
81
+ it('should return a JSON if the response contains Content-Type: application/json in the response', async () => {
82
+ const expectedResponse = JSON.stringify({ example: faker.word.words() });
83
+
84
+ mockFetch.mockResolvedValueOnce(
85
+ createResponse(200, expectedResponse, {
86
+ 'Content-Type': 'application/json',
87
+ }),
88
+ );
89
+
90
+ const response = await httpClient.sendRequest(params);
91
+
92
+ expect(response).toEqual(
93
+ new HttpResponse({
94
+ statusCode: 200,
95
+ body: expectedResponse,
96
+ }),
97
+ );
98
+ });
99
+
100
+ it('should return text if the response do not contain the Content-Type: application/json in the response', async () => {
101
+ mockFetch.mockResolvedValueOnce(createResponse(200, {}));
102
+
103
+ const response = await httpClient.sendRequest(params);
104
+
105
+ expect(response).toEqual({
106
+ statusCode: 200,
107
+ body: expect.any(String),
108
+ });
109
+ });
110
+
111
+ it('should throw exception if the response is not OK', async () => {
112
+ mockFetch.mockResolvedValueOnce(
113
+ createResponse(faker.number.int({ min: 400 }), {}),
114
+ );
115
+
116
+ await expect(() => httpClient.sendRequest(params)).rejects.toThrow();
117
+ });
118
+ });
119
+
120
+ describe('handleHttpErrorStatusCodes', () => {
121
+ it('should throw exception if the handleHttpErrorStatusCodes does not contain the status code to handle', async () => {
122
+ mockFetch.mockResolvedValueOnce(createResponse(404, {}));
123
+
124
+ await expect(() =>
125
+ httpClient.sendRequest({
126
+ ...params,
127
+ handleHttpErrorStatusCodes: [401],
128
+ }),
129
+ ).rejects.toThrow();
130
+ });
131
+
132
+ it('should return response if the handleHttpErrorStatusCodes contains the status code to handle', async () => {
133
+ mockFetch.mockResolvedValueOnce(createResponse(404, {}));
134
+
135
+ const response = await httpClient.sendRequest({
136
+ ...params,
137
+ handleHttpErrorStatusCodes: [404],
138
+ });
139
+
140
+ expect(response).toEqual({
141
+ statusCode: 404,
142
+ body: expect.anything(),
143
+ });
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,52 @@
1
+ import { HttpResponse } from '../HttpClient';
2
+ import { faker } from '@faker-js/faker';
3
+
4
+ describe('HttpResponse', () => {
5
+ describe('hasHttpStatus', () => {
6
+ it('should return true if the statusCode is as expected', () => {
7
+ const httpResponse = new HttpResponse({
8
+ statusCode: 200,
9
+ body: undefined,
10
+ });
11
+
12
+ const result = httpResponse.hasHttpStatus(200);
13
+
14
+ expect(result).toBeTruthy();
15
+ });
16
+
17
+ it('should return false if the statusCode is not as expected', () => {
18
+ const httpResponse = new HttpResponse({
19
+ statusCode: 200,
20
+ body: undefined,
21
+ });
22
+
23
+ const result = httpResponse.hasHttpStatus(faker.number.int({ min: 201 }));
24
+
25
+ expect(result).toBeFalsy();
26
+ });
27
+ });
28
+
29
+ describe('isNotFound', () => {
30
+ it('should return true if the statusCode is 404', () => {
31
+ const httpResponse = new HttpResponse({
32
+ statusCode: 404,
33
+ body: undefined,
34
+ });
35
+
36
+ const result = httpResponse.isNotFound();
37
+
38
+ expect(result).toBeTruthy();
39
+ });
40
+
41
+ it('should return false if the statusCode is different than 404', () => {
42
+ const httpResponse = new HttpResponse({
43
+ statusCode: 200,
44
+ body: undefined,
45
+ });
46
+
47
+ const result = httpResponse.isNotFound();
48
+
49
+ expect(result).toBeFalsy();
50
+ });
51
+ });
52
+ });