Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 55 additions & 22 deletions src/__tests__/terminalCloudAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Client from "../client";
import TerminalCloudAPI from "../services/terminalCloudAPI";
import { terminal } from "../typings";
import { EnvironmentEnum } from "../config";
import HttpClientException from "../httpClient/httpClientException";

let client: Client;
let terminalCloudAPI: TerminalCloudAPI;
Expand All @@ -31,29 +32,29 @@ describe("Terminal Cloud API", (): void => {

const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();

const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);
const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);

expect(typeof requestResponse).toBe("string");
expect(requestResponse).toEqual("ok");
});
expect(typeof requestResponse).toBe("string");
expect(requestResponse).toEqual("ok");
});

test("should get an error after async payment request", async (): Promise<void> => {
scope.post("/async").reply(200, asyncErrorRes);
test("should get an error after async payment request", async (): Promise<void> => {
scope.post("/async").reply(200, asyncErrorRes);

const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();

const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);
const requestResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);

if (typeof requestResponse === "object") {
expect(requestResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
expect(requestResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Reject");
} else {
throw new Error("Expected structured response, but got raw string");
}
});
if (typeof requestResponse === "object") {
expect(requestResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
expect(requestResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Reject");
} else {
throw new Error("Expected structured response, but got raw string");
}
});

test("should make a sync payment request", async (): Promise<void> => {
scope.post("/sync").reply(200, syncRes);
test("should make a sync payment request", async (): Promise<void> => {
scope.post("/sync").reply(200, syncRes);

const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
const terminalAPIResponse: terminal.TerminalApiResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
Expand Down Expand Up @@ -168,6 +169,7 @@ describe("Terminal Cloud API", (): void => {
const terminalApiHost = "https://terminal-api-test.adyen.com";

const client = new Client({ apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST });

const terminalCloudAPI = new TerminalCloudAPI(client);

const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
Expand All @@ -191,14 +193,45 @@ describe("Terminal Cloud API", (): void => {
},
});

try {
await terminalCloudAPI.sync(terminalAPIPaymentRequest);
fail("No exception was thrown");
try {
await terminalCloudAPI.sync(terminalAPIPaymentRequest);
fail("No exception was thrown");
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e).toBeInstanceOf(Error);
}

});
});

test("async should skip 308 redirect", async (): Promise<void> => {

const terminalApiHost = "https://terminal-api-test.adyen.com";

const client = new Client({ apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST, enable308Redirect: false });
const terminalCloudAPI = new TerminalCloudAPI(client);

const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
// custom value to trigger mock 308 response
terminalAPIPaymentRequest.SaleToPOIRequest.MessageHeader.SaleID = "response-with-redirect";

// Mock first request: returns a 308 redirect with Location header
nock(terminalApiHost)
.post("/async", (body) => {
return body?.SaleToPOIRequest?.MessageHeader?.SaleID === "response-with-redirect";
})
.reply(308, "", { Location: `${terminalApiHost}/async?redirect=false` });


// Must throw an error
try {
await terminalCloudAPI.async(terminalAPIPaymentRequest);
fail("No exception was thrown");
} catch (e: unknown) {
expect(e).toBeInstanceOf(HttpClientException);
if (e instanceof HttpClientException) {
expect(e.statusCode).toBe(308);
}
}
});

});

Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ interface ConfigConstructor {
terminalApiLocalEndpoint?: string;
liveEndpointUrlPrefix?: string; // must be provided for LIVE integration
region?: RegionEnum; // must be provided for Terminal API integration
enable308Redirect?: boolean; // enabling redirect upon 308 response status
}

const DEFAULT_TIMEOUT = 30000; // Default timeout value (30 sec)
Expand All @@ -67,6 +68,8 @@ class Config {
public terminalApiLocalEndpoint?: string;
public liveEndpointUrlPrefix?: string;
public region?: RegionEnum;
public enable308Redirect?: boolean;


public constructor(options: ConfigConstructor = {}) {
if (options.username) this.username = options.username;
Expand All @@ -82,6 +85,8 @@ class Config {
if (options.terminalApiLocalEndpoint) this.terminalApiLocalEndpoint = options.terminalApiLocalEndpoint;
if (options.liveEndpointUrlPrefix) this.liveEndpointUrlPrefix = options.liveEndpointUrlPrefix;
if (options.region) this.region = options.region;
this.enable308Redirect = options.enable308Redirect ?? true; // enabled by default

}

/**
Expand Down
23 changes: 16 additions & 7 deletions src/httpClient/httpURLConnectionClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class HttpURLConnectionClient implements ClientInterface {
requestOptions.headers[ApiConstants.CONTENT_TYPE] = ApiConstants.APPLICATION_JSON_TYPE;

const httpConnection: ClientRequest = this.createRequest(endpoint, requestOptions, config.applicationName);
return this.doRequest(httpConnection, json);

return this.doRequest(httpConnection, json, config.enable308Redirect ?? true);
}

// create Request object
Expand Down Expand Up @@ -142,8 +143,15 @@ class HttpURLConnectionClient implements ClientInterface {
return req;
}

// invoke request
private doRequest(connectionRequest: ClientRequest, json: string): Promise<string> {
/**
* Invoke the request
* @param connectionRequest The request
* @param json The payload
* @param allowRedirect Whether to allow redirect upon 308 response status code
* @returns Promise with the API response
*/
private doRequest(connectionRequest: ClientRequest, json: string, allowRedirect: boolean): Promise<string> {

return new Promise((resolve, reject): void => {
connectionRequest.flushHeaders();

Expand Down Expand Up @@ -173,8 +181,8 @@ class HttpURLConnectionClient implements ClientInterface {
reject(new Error("The connection was terminated while the message was still being sent"));
}

// Handle 308 redirect
if (res.statusCode && res.statusCode === 308) {
// Handle 308 redirect (when enabled)
if (allowRedirect && res.statusCode && res.statusCode === 308) {
const location = res.headers["location"];
if (location) {
// follow the redirect
Expand All @@ -195,7 +203,8 @@ class HttpURLConnectionClient implements ClientInterface {
};
const clientRequestFn = url.protocol === "https:" ? httpsRequest : httpRequest;
const redirectedRequest: ClientRequest = clientRequestFn(newRequestOptions);
const redirectResponse = this.doRequest(redirectedRequest, json);
// To prevent potential redirect loops, disable further redirects for this new request.
const redirectResponse = this.doRequest(redirectedRequest, json, false as boolean);
return resolve(redirectResponse);
} catch (err) {
return reject(err);
Expand Down Expand Up @@ -230,7 +239,7 @@ class HttpURLConnectionClient implements ClientInterface {
} catch (e) {
// parsing error
exception = new HttpClientException({
message: `HTTP Exception: ${response.statusCode}. Error parsing response: ${(e as Error).message}`,
message: `HTTP Exception: ${response.statusCode}. Error ${(e as Error).message} while parsing response: ${response.body}`,
statusCode: response.statusCode,
responseHeaders: response.headers,
responseBody: response.body,
Expand Down