Skip to content

Commit f94c4b5

Browse files
authored
Merge pull request #1524 from Adyen/sonar-handle-exceptions
Sonar handle exceptions
2 parents f654498 + 6ad97fa commit f94c4b5

File tree

4 files changed

+158
-64
lines changed

4 files changed

+158
-64
lines changed

src/__tests__/httpClient.spec.ts

Lines changed: 92 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import ApiException from "../services/exception/apiException";
66
import HttpClientException from "../httpClient/httpClientException";
77
import { binlookup } from "../typings";
88
import { ApiConstants } from "../constants/apiConstants";
9-
import {paymentMethodsSuccess} from "../__mocks__/checkout/paymentMethodsSuccess";
9+
import { paymentMethodsSuccess } from "../__mocks__/checkout/paymentMethodsSuccess";
1010
import Config from "../config";
1111
import LibraryConstants from "../constants/libraryConstants";
1212

@@ -28,7 +28,7 @@ const threeDSAvailabilitySuccess = {
2828
type errorType = "HttpClientException" | "ApiException";
2929
type testOptions = { errorType: errorType; errorMessageContains?: string; errorMessageEquals?: string };
3030

31-
const getResponse = async ({apiKey , environment }: { apiKey: string; environment: Environment}, cb: (scope: Interceptor) => testOptions): Promise<void> => {
31+
const getResponse = async ({ apiKey, environment }: { apiKey: string; environment: Environment }, cb: (scope: Interceptor) => testOptions): Promise<void> => {
3232
const client = new Client({ apiKey, environment });
3333
const binLookup = new BinLookupAPI(client);
3434

@@ -41,31 +41,86 @@ const getResponse = async ({apiKey , environment }: { apiKey: string; environmen
4141
await binLookup.BinLookupApi.get3dsAvailability(threeDSAvailabilitySuccess);
4242
fail("request should fail");
4343
} catch (e) {
44-
if(e instanceof ErrorException){
44+
if (e instanceof ErrorException) {
4545
if (errorMessageEquals) expect(e.message).toEqual(errorMessageEquals);
4646
if (errorMessageContains) expect(e.message.toLowerCase()).toContain(errorMessageContains);
4747
} else {
4848
fail();
4949
}
50-
50+
5151
}
5252
};
5353

5454
describe("HTTP Client", function (): void {
55-
it.each`
56-
apiKey | environment | withError | args | errorType | contains | equals
57-
${""} | ${"TEST"} | ${true} | ${["mocked_error_response"]} | ${"ApiException"} | ${"mocked_error_response"} | ${""}
58-
${"MOCKED_API_KEY"} | ${"TEST"} | ${true} | ${["some_error"]} | ${"ApiException"} | ${""} | ${"some_error"}
59-
${"API_KEY"} | ${"TEST"} | ${false} | ${[401, { status: 401, message: "Invalid Request", errorCode: "171", errorType: "validationError"}]} | ${"HttpClientException"} | ${""} | ${"HTTP Exception: 401. null: Invalid Request"}
60-
${"API_KEY"} | ${"TEST"} | ${false} | ${[401, {}]} | ${"HttpClientException"} | ${""} | ${"HTTP Exception: 401. null"}
61-
${"API_KEY"} | ${"TEST"} | ${false} | ${[401, "fail"]} | ${"HttpClientException"} | ${""} | ${"HTTP Exception: 401. null"}
62-
`("Should return $errorType, $contains, $equals", async ({ apiKey, environment, withError, args, errorType, contains, equals }) => {
63-
await getResponse({ apiKey, environment }, (scope) => {
64-
if (withError) scope.replyWithError(args[0]);
65-
else scope.reply(args[0], args[1]);
66-
67-
return { errorType, errorMessageContains: contains, errorMessageEquals: equals };
68-
});
55+
56+
test("Should return ApiException with message containing 'mocked_error_response'", async () => {
57+
await getResponse(
58+
{ apiKey: "", environment: "TEST" },
59+
(scope) => {
60+
scope.replyWithError("mocked_error_response");
61+
return {
62+
errorType: "ApiException",
63+
errorMessageContains: "",
64+
errorMessageEquals: "mocked_error_response"
65+
};
66+
}
67+
);
68+
});
69+
70+
test("Should return ApiException with message equal to 'some_error'", async () => {
71+
await getResponse(
72+
{ apiKey: "MOCKED_API_KEY", environment: "TEST" },
73+
(scope) => {
74+
scope.replyWithError("some_error");
75+
return {
76+
errorType: "ApiException",
77+
errorMessageContains: "",
78+
errorMessageEquals: "some_error"
79+
};
80+
}
81+
);
82+
});
83+
84+
test("Should return HttpClientException with message equal to 'HTTP Exception: 401. null: Invalid Request'", async () => {
85+
await getResponse(
86+
{ apiKey: "API_KEY", environment: "TEST" },
87+
(scope) => {
88+
scope.reply(401, { status: 401, message: "Invalid Request", errorCode: "171", errorType: "validationError" });
89+
return {
90+
errorType: "HttpClientException",
91+
errorMessageContains: "",
92+
errorMessageEquals: "HTTP Exception: 401. null: Invalid Request"
93+
};
94+
}
95+
);
96+
});
97+
98+
test("Should return HttpClientException with message equal to 'HTTP Exception: 401. null'", async () => {
99+
await getResponse(
100+
{ apiKey: "API_KEY", environment: "TEST" },
101+
(scope) => {
102+
scope.reply(401, {});
103+
return {
104+
errorType: "HttpClientException",
105+
errorMessageContains: "",
106+
errorMessageEquals: "HTTP Exception: 401. null"
107+
};
108+
}
109+
);
110+
});
111+
112+
test("Should return HttpClientException with message starting with 'HTTP Exception: 401'", async () => {
113+
await getResponse(
114+
{ apiKey: "API_KEY", environment: "TEST" },
115+
(scope) => {
116+
scope.reply(401, "fail");
117+
return {
118+
errorType: "HttpClientException",
119+
errorMessageContains: "http exception: 401", // must be case insensitive assertion
120+
errorMessageEquals: ""
121+
};
122+
}
123+
);
69124
});
70125

71126
test("should succeed on get 3ds availability", async function (): Promise<void> {
@@ -83,9 +138,9 @@ describe("HTTP Client", function (): void {
83138
const binLookupService = new BinLookupAPI(client);
84139
const scope = nock("https://pal-test.adyen.com/pal/servlet/BinLookup/v54", {
85140
reqheaders: {
86-
"Content-Type" : ApiConstants.APPLICATION_JSON_TYPE,
87-
"foo" : "bar"
88-
},
141+
"Content-Type": ApiConstants.APPLICATION_JSON_TYPE,
142+
"foo": "bar"
143+
},
89144
})
90145
.get("/")
91146
.reply(200);
@@ -99,81 +154,79 @@ describe("HTTP Client", function (): void {
99154
scope.post("/get3dsAvailability")
100155
.reply(200, threeDSAvailabilitySuccessResponse);
101156

102-
const requestOptions = { headers: { foo : "bar" }};
157+
const requestOptions = { headers: { foo: "bar" } };
103158
const response = await binLookupService.BinLookupApi.get3dsAvailability(threeDSAvailabilityRequest, requestOptions);
104-
expect(response).toEqual< binlookup.ThreeDSAvailabilityResponse>(threeDSAvailabilitySuccessResponse);
105-
106-
console.log("requestOptions", requestOptions);
159+
expect(response).toEqual<binlookup.ThreeDSAvailabilityResponse>(threeDSAvailabilitySuccessResponse);
107160
});
108161

109-
test("should add default applicationInfo to the headers", async (): Promise<void> => {
162+
test("should add default applicationInfo to the headers", async (): Promise<void> => {
110163
const client = createClient();
111164
const checkout = new CheckoutAPI(client);
112165

113166
const expectedUserAgent = `${LibraryConstants.LIB_NAME}/${LibraryConstants.LIB_VERSION}`;
114167
const expectedLibraryName = LibraryConstants.LIB_NAME;
115168
const expectedLibraryVersion = LibraryConstants.LIB_VERSION;
116-
169+
117170
const scope = nock("https://checkout-test.adyen.com/v71", {
118171
reqheaders: {
119172
"adyen-library-name": (headerValue) => {
120-
expect(headerValue).toBeTruthy();
173+
expect(headerValue).toBeTruthy();
121174
expect(headerValue).toEqual(expectedLibraryName);
122175
return true;
123176
},
124177
"adyen-library-version": (headerValue) => {
125-
expect(headerValue).toBeTruthy();
178+
expect(headerValue).toBeTruthy();
126179
expect(headerValue).toEqual(expectedLibraryVersion);
127180
return true;
128181
},
129182
"user-agent": (headerValue) => {
130-
expect(headerValue).toBeTruthy();
183+
expect(headerValue).toBeTruthy();
131184
expect(headerValue).toEqual(expectedUserAgent);
132185
return true;
133186
}
134187
}
135188
});
136189

137190
scope.post("/paymentMethods").reply(200, paymentMethodsSuccess);
138-
const response = await checkout.PaymentsApi.paymentMethods({"merchantAccount": "testMerchantAccount"});
191+
const response = await checkout.PaymentsApi.paymentMethods({ "merchantAccount": "testMerchantAccount" });
139192

140193
expect(response.paymentMethods).toBeTruthy();
141-
});
194+
});
142195

143-
test("should add custom applicationInfo to the headers", async (): Promise<void> => {
196+
test("should add custom applicationInfo to the headers", async (): Promise<void> => {
144197
const client = createClient();
145198
client.config.applicationName = "testApp";
146199
const checkout = new CheckoutAPI(client);
147200

148201
const expectedUserAgent = `testApp ${LibraryConstants.LIB_NAME}/${LibraryConstants.LIB_VERSION}`; // include applicationName too
149202
const expectedLibraryName = LibraryConstants.LIB_NAME;
150203
const expectedLibraryVersion = LibraryConstants.LIB_VERSION;
151-
204+
152205
const scope = nock("https://checkout-test.adyen.com/v71", {
153206
reqheaders: {
154207
"adyen-library-name": (headerValue) => {
155-
expect(headerValue).toBeTruthy();
208+
expect(headerValue).toBeTruthy();
156209
expect(headerValue).toEqual(expectedLibraryName);
157210
return true;
158211
},
159212
"adyen-library-version": (headerValue) => {
160-
expect(headerValue).toBeTruthy();
213+
expect(headerValue).toBeTruthy();
161214
expect(headerValue).toEqual(expectedLibraryVersion);
162215
return true;
163216
},
164217
"user-agent": (headerValue) => {
165-
expect(headerValue).toBeTruthy();
218+
expect(headerValue).toBeTruthy();
166219
expect(headerValue).toEqual(expectedUserAgent);
167220
return true;
168221
}
169222
}
170223
});
171224

172225
scope.post("/paymentMethods").reply(200, paymentMethodsSuccess);
173-
const response = await checkout.PaymentsApi.paymentMethods({"merchantAccount": "testMerchantAccount"});
226+
const response = await checkout.PaymentsApi.paymentMethods({ "merchantAccount": "testMerchantAccount" });
174227

175228
expect(response.paymentMethods).toBeTruthy();
176-
});
229+
});
177230

178231
});
179232

src/helpers/getJsonResponse.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,43 @@ import { IRequest } from "../typings/requestOptions";
2323
async function getJsonResponse<T>(resource: Resource, jsonRequest: T | string, requestOptions?: IRequest.Options): Promise<string>;
2424
async function getJsonResponse<T, R>(resource: Resource, jsonRequest: T | string, requestOptions?: IRequest.Options): Promise<R>;
2525

26+
/**
27+
* Makes the API call and returns the parsed JSON response.
28+
*
29+
* @template T - The type of the request payload.
30+
* @template R - The expected type of the parsed JSON response.
31+
* @param resource - The API resource responsible for handling the request.
32+
* @param jsonRequest - The request payload, either as an object or a JSON string.
33+
* @param requestOptions - Optional request options to customize the request.
34+
* @returns A promise that resolves to the parsed JSON response of type `R`, or the string "ok" for TerminalAPI responses.
35+
*/
2636
async function getJsonResponse<T, R>(
2737
resource: Resource,
2838
jsonRequest: T | string,
2939
requestOptions: IRequest.Options = {},
3040
): Promise<R | string> {
3141
const request = typeof jsonRequest === "string" ? jsonRequest : JSON.stringify(jsonRequest);
3242
const response = await resource.request(request, requestOptions);
43+
44+
if (!response) {
45+
return "" as string;
46+
}
47+
48+
if (typeof response !== "string") {
49+
return response;
50+
}
51+
52+
if (response === "ok") {
53+
// handling TerminalAPI responses
54+
return response;
55+
}
56+
3357
try {
34-
return typeof response === "string" ? JSON.parse(response) : response;
58+
return JSON.parse(response);
3559
} catch (e) {
36-
return response;
60+
console.warn("Unexpected error in getJsonResponse:", (e as Error).message);
61+
return response; // or: return response as R | string;
3762
}
3863
}
64+
3965
export default getJsonResponse;

src/httpClient/clientInterface.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ interface ClientInterface {
2424
request(
2525
endpoint: string, json: string, config: Config, isApiKeyRequired: boolean, requestOptions?: IRequest.Options,
2626
): Promise<string>;
27-
post(endpoint: string, postParameters: [string, string][], config: Config): Promise<string>;
2827
proxy?: AgentOptions;
2928
}
3029

0 commit comments

Comments
 (0)