Skip to content

Commit 60f2a3d

Browse files
authored
Merge pull request #1540 from Adyen/support-308-redirect
Terminal API: handle 308 response status
2 parents 3eb2d5c + ab933d2 commit 60f2a3d

File tree

3 files changed

+246
-73
lines changed

3 files changed

+246
-73
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import HttpURLConnectionClient from "../httpClient/httpURLConnectionClient";
2+
3+
describe("HttpURLConnectionClient", () => {
4+
let client: HttpURLConnectionClient;
5+
6+
beforeEach(() => {
7+
client = new HttpURLConnectionClient();
8+
});
9+
10+
describe("verifyLocation", () => {
11+
test.each([
12+
"https://example.adyen.com/path",
13+
"https://sub.adyen.com",
14+
"http://another.adyen.com/a/b/c?q=1",
15+
"https://checkout-test.adyen.com",
16+
])("should return true for valid adyen.com domain: %s", (location) => {
17+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
18+
// @ts-ignore - testing a private method
19+
expect(client.verifyLocation(location)).toBe(true);
20+
});
21+
22+
test.each([
23+
"https://example.ADYEN.com/path",
24+
"HTTPS://sub.adyen.COM",
25+
])("should be case-insensitive for valid adyen.com domain: %s", (location) => {
26+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
27+
// @ts-ignore - testing a private method
28+
expect(client.verifyLocation(location)).toBe(true);
29+
});
30+
31+
test.each([
32+
"https://adyen.com.evil.com/path",
33+
"https://evil-adyen.com",
34+
"http://adyen.co",
35+
"https://www.google.com",
36+
"https://adyen.com-scam.com",
37+
])("should return false for invalid domain: %s", (location) => {
38+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
39+
// @ts-ignore - testing a private method
40+
expect(client.verifyLocation(location)).toBe(false);
41+
});
42+
43+
test.each([
44+
"https://adyen.com.another.domain/path",
45+
"https://myadyen.com.org",
46+
])("should return false for domains that contain but do not end with adyen.com: %s", (location) => {
47+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
48+
// @ts-ignore - testing a private method
49+
expect(client.verifyLocation(location)).toBe(false);
50+
});
51+
52+
test.each([
53+
"not a url",
54+
"adyen.com",
55+
"//adyen.com/path",
56+
])("should return false for malformed URLs: %s", (location) => {
57+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
58+
// @ts-ignore - testing a private method
59+
expect(client.verifyLocation(location)).toBe(false);
60+
});
61+
});
62+
});

src/__tests__/terminalCloudAPI.spec.ts

Lines changed: 134 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,31 @@ import { syncRefund, syncRes, syncResEventNotification, syncResEventNotification
55
import Client from "../client";
66
import TerminalCloudAPI from "../services/terminalCloudAPI";
77
import { terminal } from "../typings";
8+
import { EnvironmentEnum } from "../config";
89

910
let client: Client;
1011
let terminalCloudAPI: TerminalCloudAPI;
1112
let scope: nock.Scope;
1213

1314
beforeEach((): void => {
14-
if (!nock.isActive()) {
15-
nock.activate();
16-
}
17-
client = createClient(process.env.ADYEN_TERMINAL_APIKEY);
15+
if (!nock.isActive()) {
16+
nock.activate();
17+
}
18+
client = createClient(process.env.ADYEN_TERMINAL_APIKEY);
1819

19-
terminalCloudAPI = new TerminalCloudAPI(client);
20-
scope = nock(`${client.config.terminalApiCloudEndpoint}`);
20+
terminalCloudAPI = new TerminalCloudAPI(client);
21+
scope = nock(`${client.config.terminalApiCloudEndpoint}`);
2122
});
2223

2324
afterEach((): void => {
24-
nock.cleanAll();
25+
nock.cleanAll();
2526
});
2627

2728
describe("Terminal Cloud API", (): void => {
28-
test("should make an async payment request", async (): Promise<void> => {
29-
scope.post("/async").reply(200, asyncRes);
29+
test("should make an async payment request", async (): Promise<void> => {
30+
scope.post("/async").reply(200, asyncRes);
3031

31-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
32+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
3233

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

@@ -54,88 +55,152 @@ describe("Terminal Cloud API", (): void => {
5455
test("should make a sync payment request", async (): Promise<void> => {
5556
scope.post("/sync").reply(200, syncRes);
5657

57-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
58-
const terminalAPIResponse: terminal.TerminalApiResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
58+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
59+
const terminalAPIResponse: terminal.TerminalApiResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
5960

60-
expect(terminalAPIResponse.SaleToPOIResponse?.PaymentResponse).toBeDefined();
61-
expect(terminalAPIResponse.SaleToPOIResponse?.MessageHeader).toBeDefined();
62-
});
61+
expect(terminalAPIResponse.SaleToPOIResponse?.PaymentResponse).toBeDefined();
62+
expect(terminalAPIResponse.SaleToPOIResponse?.MessageHeader).toBeDefined();
63+
});
6364

64-
test("should make a sync payment request with additional attributes", async (): Promise<void> => {
65-
scope.post("/sync").reply(200, syncTerminalPaymentResponse);
65+
test("should make a sync payment request with additional attributes", async (): Promise<void> => {
66+
scope.post("/sync").reply(200, syncTerminalPaymentResponse);
6667

67-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
68+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
6869

69-
await expect(async () => {
70-
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
71-
expect(terminalAPIResponse.SaleToPOIResponse?.PaymentResponse).toBeDefined();
72-
expect(terminalAPIResponse.SaleToPOIResponse?.MessageHeader).toBeDefined();
73-
}).not.toThrow();
70+
await expect(async () => {
71+
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
72+
expect(terminalAPIResponse.SaleToPOIResponse?.PaymentResponse).toBeDefined();
73+
expect(terminalAPIResponse.SaleToPOIResponse?.MessageHeader).toBeDefined();
74+
}).not.toThrow();
7475

75-
});
76+
});
7677

77-
test("should return event notification Reject", async (): Promise<void> => {
78+
test("should return event notification Reject", async (): Promise<void> => {
7879

79-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
80-
scope.post("/sync").reply(200, syncResEventNotification);
80+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
81+
scope.post("/sync").reply(200, syncResEventNotification);
8182

82-
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
83+
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
8384

84-
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
85-
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Reject");
85+
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
86+
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Reject");
8687

87-
});
88+
});
8889

89-
test("should return event notification Shutdown with additional attributes", async (): Promise<void> => {
90+
test("should return event notification Shutdown with additional attributes", async (): Promise<void> => {
9091

91-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
92-
scope.post("/sync").reply(200, syncResEventNotificationWithAdditionalAttributes);
93-
94-
await expect(async () => {
95-
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
96-
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
97-
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Shutdown");
98-
expect(terminalAPIResponse.SaleToPOIRequest?.MessageHeader).toBeDefined();
99-
}).not.toThrow();
100-
});
92+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
93+
scope.post("/sync").reply(200, syncResEventNotificationWithAdditionalAttributes);
10194

102-
test("should return event notification with unknown enum", async (): Promise<void> => {
95+
await expect(async () => {
96+
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
97+
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
98+
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("Shutdown");
99+
expect(terminalAPIResponse.SaleToPOIRequest?.MessageHeader).toBeDefined();
100+
}).not.toThrow();
101+
});
103102

104-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
105-
scope.post("/sync").reply(200, syncResEventNotificationWithUnknownEnum);
103+
test("should return event notification with unknown enum", async (): Promise<void> => {
106104

107-
await expect(async () => {
108-
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
109-
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
110-
// EventToNotify is unknown, so it holds whatever value is found in the payload
111-
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("this is unknown");
105+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
106+
scope.post("/sync").reply(200, syncResEventNotificationWithUnknownEnum);
112107

113-
}).not.toThrow();
114-
});
108+
await expect(async () => {
109+
const terminalAPIResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
110+
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification).toBeDefined();
111+
// EventToNotify is unknown, so it holds whatever value is found in the payload
112+
expect(terminalAPIResponse.SaleToPOIRequest?.EventNotification?.EventToNotify).toBe("this is unknown");
115113

116-
test("should make an async refund request", async (): Promise<void> => {
117-
scope.post("/sync").reply(200, syncRes);
114+
}).not.toThrow();
115+
});
118116

119-
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
120-
const terminalAPIResponse: terminal.TerminalApiResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
117+
test("should make an async refund request", async (): Promise<void> => {
118+
scope.post("/sync").reply(200, syncRes);
121119

122-
const pOITransactionId = terminalAPIResponse.SaleToPOIResponse!.PaymentResponse!.POIData!.POITransactionID;
123-
expect(pOITransactionId).toBeTruthy();
120+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
121+
const terminalAPIResponse: terminal.TerminalApiResponse = await terminalCloudAPI.sync(terminalAPIPaymentRequest);
124122

125-
scope.post("/sync").reply(200, syncRefund);
123+
const pOITransactionId = terminalAPIResponse.SaleToPOIResponse!.PaymentResponse!.POIData!.POITransactionID;
124+
expect(pOITransactionId).toBeTruthy();
126125

127-
const terminalAPIRefundRequest = createTerminalAPIRefundRequest(pOITransactionId);
128-
const id = Math.floor(Math.random() * Math.floor(10000000)).toString();
129-
terminalAPIRefundRequest.SaleToPOIRequest.MessageHeader.ServiceID = id;
130-
const saleToAcquirerData: terminal.SaleToAcquirerData = new terminal.SaleToAcquirerData();
131-
saleToAcquirerData.currency = "EUR";
132-
terminalAPIRefundRequest.SaleToPOIRequest.ReversalRequest!.SaleData!.SaleToAcquirerData = saleToAcquirerData;
133-
const terminalAPIRefundResponse = await terminalCloudAPI.sync(terminalAPIRefundRequest);
126+
scope.post("/sync").reply(200, syncRefund);
134127

135-
expect(terminalAPIRefundResponse.SaleToPOIResponse?.ReversalResponse?.Response.Result).toBe("Success");
136-
}, 20000);
137-
});
128+
const terminalAPIRefundRequest = createTerminalAPIRefundRequest(pOITransactionId);
129+
const id = Math.floor(Math.random() * Math.floor(10000000)).toString();
130+
terminalAPIRefundRequest.SaleToPOIRequest.MessageHeader.ServiceID = id;
131+
const saleToAcquirerData: terminal.SaleToAcquirerData = new terminal.SaleToAcquirerData();
132+
saleToAcquirerData.currency = "EUR";
133+
terminalAPIRefundRequest.SaleToPOIRequest.ReversalRequest!.SaleData!.SaleToAcquirerData = saleToAcquirerData;
134+
const terminalAPIRefundResponse = await terminalCloudAPI.sync(terminalAPIRefundRequest);
135+
136+
expect(terminalAPIRefundResponse.SaleToPOIResponse?.ReversalResponse?.Response.Result).toBe("Success");
137+
}, 20000);
138+
139+
test("async should handle 308", async (): Promise<void> => {
138140

141+
const terminalApiHost = "https://terminal-api-test.adyen.com";
142+
143+
const client = new Client({ apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST });
144+
const terminalCloudAPI = new TerminalCloudAPI(client);
145+
146+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
147+
// custom value to trigger mock 308 response
148+
terminalAPIPaymentRequest.SaleToPOIRequest.MessageHeader.SaleID = "response-with-redirect";
149+
150+
// Mock first request: returns a 308 redirect with Location header
151+
nock(terminalApiHost)
152+
.post("/async", (body) => {
153+
return body?.SaleToPOIRequest?.MessageHeader?.SaleID === "response-with-redirect";
154+
})
155+
.reply(308, "", { Location: `${terminalApiHost}/async?redirect=false` });
156+
157+
// Mock follow-up request: returns successful response 'ok'
158+
nock(terminalApiHost)
159+
.post("/async?redirect=false")
160+
.reply(200, "ok");
161+
162+
const terminalAPIResponse = await terminalCloudAPI.async(terminalAPIPaymentRequest);
163+
164+
expect(terminalAPIResponse).toEqual("ok");
165+
});
166+
167+
test("sync should validate 308 location header", async (): Promise<void> => {
168+
const terminalApiHost = "https://terminal-api-test.adyen.com";
169+
170+
const client = new Client({ apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST });
171+
const terminalCloudAPI = new TerminalCloudAPI(client);
172+
173+
const terminalAPIPaymentRequest = createTerminalAPIPaymentRequest();
174+
// custom value to trigger mock 308 response
175+
terminalAPIPaymentRequest.SaleToPOIRequest.MessageHeader.SaleID = "response-with-redirect";
176+
177+
// Mock first request: returns a 308 redirect with invalid Location header
178+
nock(terminalApiHost)
179+
.post("/sync", (body) => {
180+
return body?.SaleToPOIRequest?.MessageHeader?.SaleID === "response-with-redirect";
181+
})
182+
.reply(308, "", { Location: "https://example.org/sync?redirect=false" });
183+
184+
// Mock follow-up request: returns successful response
185+
nock(terminalApiHost)
186+
.post("/sync?redirect=false")
187+
.reply(200, {
188+
SaleToPOIResponse: {
189+
PaymentResponse: { Response: "Authorised" },
190+
MessageHeader: { SaleID: "001-308" },
191+
},
192+
});
193+
194+
try {
195+
await terminalCloudAPI.sync(terminalAPIPaymentRequest);
196+
fail("No exception was thrown");
197+
} catch (e) {
198+
expect(e).toBeInstanceOf(Error);
199+
}
200+
201+
});
202+
203+
});
139204

140205
export const syncTerminalPaymentResponse = {
141206
"SaleToPOIResponse": {

0 commit comments

Comments
 (0)