Skip to content

Commit 61a9bf5

Browse files
authored
Merge pull request #278 from wolfpackthatcodes/refactoring/improving-tests
Refactoring & improving tests
2 parents d1eab84 + 2216d27 commit 61a9bf5

24 files changed

+1702
-1337
lines changed

code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"test:ui": "yarn run test --ui --coverage --open=false"
5353
},
5454
"devDependencies": {
55+
"@faker-js/faker": "^9.2.0",
5556
"@types/node": "^22.9.3",
5657
"@typescript-eslint/eslint-plugin": "^7.0.0",
5758
"@typescript-eslint/parser": "^6.21.0",

code/src/http/httpClient.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
InvalidHeaderFormatException,
1414
InvalidRequestBodyFormatException,
1515
} from './exceptions';
16+
import { MultipartDataSerializer } from './serializers';
1617

1718
export default class HttpClient {
1819
/**
@@ -25,7 +26,7 @@ export default class HttpClient {
2526
/**
2627
* The request body data.
2728
*
28-
* @var {object | string | undefined}
29+
* @var { object | string | undefined}
2930
*/
3031
private body?: object | string;
3132

@@ -41,7 +42,14 @@ export default class HttpClient {
4142
*
4243
* @var {MockedResponse}
4344
*/
44-
private mockedResponses: MockedResponses;
45+
private readonly mockedResponses: MockedResponses;
46+
47+
/**
48+
* The Multipart Data Serializer instance.
49+
*
50+
* @var {MultipartDataSerializer}
51+
*/
52+
private readonly multipartDataSerializer: MultipartDataSerializer;
4553

4654
/**
4755
* Number of attempts for the request.
@@ -65,18 +73,18 @@ export default class HttpClient {
6573
private retries: number = 0;
6674

6775
/**
68-
* The number of milliseconds to wait between retries.
76+
* The callback that will determine if the request should be retried.
6977
*
70-
* @var {number}
78+
* @var {function}
7179
*/
72-
private retryDelay: number = 0;
80+
private retryCallback?: (response: Response | undefined, request: this, error?: unknown) => boolean | null;
7381

7482
/**
75-
* The callback that will determine if the request should be retried.
83+
* The number of milliseconds to wait between retries.
7684
*
77-
* @var {function}
85+
* @var {number}
7886
*/
79-
private retryCallback?: (response: Response | undefined, request: this, error?: unknown) => boolean | null;
87+
private retryDelay: number = 0;
8088

8189
/**
8290
* The URL for the request.
@@ -100,6 +108,7 @@ export default class HttpClient {
100108
*/
101109
constructor(baseUrl?: string, options?: object) {
102110
this.mockedResponses = new MockedResponses();
111+
this.multipartDataSerializer = new MultipartDataSerializer();
103112

104113
this.setBaseUrl(baseUrl);
105114
this.setOptions(options);
@@ -144,7 +153,7 @@ export default class HttpClient {
144153
*/
145154
public asForm(): this {
146155
this.setBodyFormat('FormData');
147-
this.contentType('multipart/form-data');
156+
this.contentType(`multipart/form-data; boundary=${this.multipartDataSerializer.getBoundary()}`);
148157

149158
return this;
150159
}
@@ -210,14 +219,11 @@ export default class HttpClient {
210219
let url = this.url
211220
.split('://')
212221
.map((urlSlug) => urlSlug.replace(/\/{2,}/g, '/'))
213-
.join('://');
214-
215-
if (!url.endsWith('/')) {
216-
url += '/';
217-
}
222+
.join('://')
223+
.replace(/\/$/, '');
218224

219225
if (!(url.startsWith('http://') || url.startsWith('https://'))) {
220-
url = this.baseUrl.replace(/\/$/, '') + url;
226+
url = this.baseUrl.replace(/\/$/, '') + '/' + url.replace(/^\//, '');
221227
}
222228

223229
if (this.urlQueryParameters !== undefined) {
@@ -376,13 +382,19 @@ export default class HttpClient {
376382
throw new InvalidRequestBodyFormatException('Cannot parse a string as FormData.');
377383
}
378384

379-
const formData = new FormData();
385+
let formData: FormData;
386+
387+
if (this.body instanceof FormData) {
388+
formData = this.body;
389+
} else {
390+
formData = new FormData();
380391

381-
for (const [key, value] of Object.entries(this.body)) {
382-
formData.append(key, value);
392+
for (const [key, value] of Object.entries(this.body)) {
393+
formData.append(key, value);
394+
}
383395
}
384396

385-
return formData;
397+
return this.multipartDataSerializer.generateMultipartPayload(formData);
386398
}
387399

388400
if (this.bodyFormat === 'URLSearchParams') {
@@ -548,11 +560,11 @@ export default class HttpClient {
548560
/**
549561
* Specify the body data of the request.
550562
*
551-
* @param {object | string} body
563+
* @param {FormData | object | string} body
552564
*
553565
* @return {void}
554566
*/
555-
private setBody(body: object | string): void {
567+
private setBody(body: FormData | object | string): void {
556568
this.body = body;
557569
}
558570

code/src/http/responses/mockedResponses.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Str from '@/support/str';
1+
import Str from '@/utils/str';
22
import { MissingMockedResponseException } from '../exceptions';
33

44
export default class MockedResponses {
@@ -44,7 +44,9 @@ export default class MockedResponses {
4444
public getMockedResponse(requestUrl: string): Promise<Response> {
4545
type ObjectKey = keyof typeof this.mockedResponses;
4646

47-
const mockedUrl = Object.keys(this.mockedResponses).find((url) => {
47+
const mockedUrl = Object.keys(this.mockedResponses).find((url: string) => {
48+
url = url.replace(/\/\*$/, '*');
49+
4850
return url === requestUrl || url === '*' ? true : Str.is(Str.start(url, '*'), requestUrl);
4951
}) as ObjectKey;
5052

code/src/http/serializers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import MultipartDataSerializer from './multipartDataSerializer';
2+
3+
export { MultipartDataSerializer };
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
export default class MultipartDataSerializer {
2+
/**
3+
* A unique boundary string used to separate parts of the multipart/form-data payload.
4+
*
5+
* @type {string}
6+
*/
7+
private readonly boundary: string;
8+
9+
/**
10+
* Create a new Multipart Data Serializer instance.
11+
*/
12+
constructor() {
13+
this.boundary = this.generateBoundary();
14+
}
15+
16+
/**
17+
* Generates a unique boundary string for a multipart/form-data payload.
18+
* The boundary ensures proper separation of parts in the payload.
19+
*
20+
* @returns {string}
21+
*/
22+
private generateBoundary(): string {
23+
return `----form-data-boundary-${Date.now()}`;
24+
}
25+
26+
/**
27+
* Creates a multipart/form-data payload from a given FormData object.
28+
*
29+
* @param {FormData} data
30+
*
31+
* @returns {string}
32+
*/
33+
public generateMultipartPayload(data: FormData): string {
34+
const lines: string[] = [];
35+
36+
data.forEach((value, key) => {
37+
lines.push(`--${this.boundary}`);
38+
39+
if (value instanceof File) {
40+
lines.push(
41+
`Content-Disposition: form-data; name="${key}"; filename="${value.name}"`,
42+
`Content-Type: ${value.type || 'application/octet-stream'}`,
43+
);
44+
lines.push('');
45+
lines.push(value as unknown as string);
46+
} else {
47+
lines.push(`Content-Disposition: form-data; name="${key}"`);
48+
lines.push('');
49+
lines.push(value);
50+
}
51+
});
52+
53+
lines.push(`--${this.boundary}--`);
54+
55+
return lines.join('\r\n');
56+
}
57+
58+
/**
59+
* Retrieves the boundary string used for the multipart/form-data payload.
60+
*
61+
* @returns {string}
62+
*/
63+
public getBoundary(): string {
64+
return this.boundary;
65+
}
66+
}
File renamed without changes.

0 commit comments

Comments
 (0)