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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
**/target/**
.DS_Store
.vscode
src/test/resources/config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,15 @@ public CloudDeviceApiResponse sync(
CloudDeviceApiSecuredResponse cloudDeviceApiSecuredResponse =
CloudDeviceApiSecuredResponse.fromJson(response);

SaleToPOISecuredMessage saleToPOISecuredResponse =
cloudDeviceApiSecuredResponse.getSaleToPOIResponse();
Comment thread
gcatanese marked this conversation as resolved.
if (saleToPOISecuredResponse == null || saleToPOISecuredResponse.getNexoBlob() == null) {
// Terminal returned an unencrypted error response (e.g. terminal unreachable)
return CloudDeviceApiResponse.fromJson(response);
}

return CloudDeviceApiResponse.fromJson(
nexoSecurityManager.decrypt(cloudDeviceApiSecuredResponse.getSaleToPOIResponse()));
nexoSecurityManager.decrypt(saleToPOISecuredResponse));
}

/**
Expand Down Expand Up @@ -190,7 +197,12 @@ public CloudDeviceApiAsyncResponse async(
// Error response: an encrypted EventNotification wrapped in a SaleToPOIRequest envelope.
// Decrypt it and surface the SaleToPOIRequest to the caller.
CloudDeviceApiSecuredRequest encryptedError = CloudDeviceApiSecuredRequest.fromJson(response);
String decryptedJson = nexoSecurityManager.decrypt(encryptedError.getSaleToPOIRequest());
SaleToPOISecuredMessage encryptedErrorRequest = encryptedError.getSaleToPOIRequest();
Comment thread
gcatanese marked this conversation as resolved.
if (encryptedErrorRequest == null || encryptedErrorRequest.getNexoBlob() == null) {
// Terminal returned an unencrypted error response (e.g. terminal unreachable)
return CloudDeviceApiAsyncResponse.fromJson(response);
}
String decryptedJson = nexoSecurityManager.decrypt(encryptedErrorRequest);
CloudDeviceApiAsyncResponse errorResponse =
CloudDeviceApiAsyncResponse.fromJson(decryptedJson);
cloudDeviceApiAsyncResponse.setSaleToPOIRequest(errorResponse.getSaleToPOIRequest());
Expand Down
81 changes: 81 additions & 0 deletions src/test/java/com/adyen/BaseIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Adyen Java API Library
*
* Copyright (c) 2025 Adyen B.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*/
package com.adyen;

import com.adyen.enums.Environment;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
* Base class for Integration tests
*
* <p>Define in src/test/resources the configuration for the tests
*
* <p>``` ADYEN_API_KEY= ADYEN_MERCHANT_ACCOUNT= ADYEN_TERMINAL_DEVICE_ID=
* ADYEN_TERMINAL_DEVICE_KEY_IDENTIFIER= ADYEN_TERMINAL_DEVICE_PASSPHRASE= ```
*/
public class BaseIntegrationTest {

private static Properties properties = null;

protected Client getClient() {
return new Client(new Config().apiKey(getApiKey()).environment(Environment.TEST));
}

protected String getApiKey() {
return getProperty("ADYEN_API_KEY");
}

protected String getMerchantAccount() {
return getProperty("ADYEN_MERCHANT_ACCOUNT");
}

protected String getTerminalDeviceId() {
return getProperty("ADYEN_TERMINAL_DEVICE_ID");
}

protected String getTerminalDeviceKeyIdentifier() {
return getProperty("ADYEN_TERMINAL_DEVICE_KEY_IDENTIFIER");
}

protected String getTerminalDevicePassphrase() {
return getProperty("ADYEN_TERMINAL_DEVICE_PASSPHRASE");
}

private Properties getProperties() {
if (properties == null) {
properties = new Properties();
try (InputStream inputStream =
BaseIntegrationTest.class.getClassLoader().getResourceAsStream("config.properties")) {
if (inputStream != null) {
properties.load(inputStream);
}
} catch (IOException e) {
// Do nothing, properties will be empty
}
}

return properties;
}
Comment thread
gcatanese marked this conversation as resolved.

private String getProperty(String name) {
String property = System.getenv(name);

if (property != null && !property.isEmpty()) {
return property;
}
property = getProperties().getProperty(name);

if (property == null || property.isEmpty()) {
throw new RuntimeException("Property " + name + " not defined");
}

return property;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package com.adyen.service.clouddevice;

import com.adyen.BaseIntegrationTest;
import com.adyen.model.clouddevice.*;
import com.adyen.model.tapi.*;
import com.adyen.security.clouddevice.EncryptionCredentialDetails;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

/**
* Verify Terminal integration: tests to send API requests to the Cloud Device API and test the
* Terminal responds as expected.
*
* <p>Don't forget to:
*
* <ul>
* <li>Enable the terminal
* <li>Enable the test to run (by removing/commenting the {@code @Disabled} annotation)
* <li>Set required variables by creating {@code src/test/resources/config.properties}:
* </ul>
*
* <pre>{@code
* # Example of config.properties
* ADYEN_API_KEY=
* ADYEN_MERCHANT_ACCOUNT=MyMerchantAccount
* ADYEN_TERMINAL_DEVICE_ID=V400m-1234567890
* ADYEN_TERMINAL_DEVICE_KEY_IDENTIFIER=
* ADYEN_TERMINAL_DEVICE_PASSPHRASE=
* }</pre>
*
* <ul>
* <li>Run one test at a time with {@code mvn test -Dtest=CloudDeviceApiTerminalIT#sendSync}
* <li>Disable the test again
* </ul>
*/
public class CloudDeviceApiTerminalIT extends BaseIntegrationTest {

@Disabled("Enable when you want to test with the Terminal")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is "special" integration test class, where tests need to be executed one by one (each test will wait for a response from the Terminal). It might be confusing, but it is the only way to test the integration with the POS terminal works.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, let's keep like this. If we have some idea in the future, we can address in a follow-up PR. I don't see it a blocker, so I'm leaving the approval.

@Test
public void sendSync() throws Exception {

CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(getClient());

CloudDeviceApiRequest cloudDeviceApiRequest =
createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());

var response =
cloudDeviceApi.sync(getMerchantAccount(), getTerminalDeviceId(), cloudDeviceApiRequest);

Assertions.assertNotNull(response);
Assertions.assertNotNull(response.getSaleToPOIResponse());
Assertions.assertEquals(getTerminalDeviceId(), response.getSaleToPOIResponse().getMessageHeader().getPOIID());
}

@Disabled("Enable when you want to test with the Terminal")
@Test
public void sendAsync() throws Exception {

CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(getClient());

CloudDeviceApiRequest cloudDeviceApiRequest =
createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());

var response =
cloudDeviceApi.async(getMerchantAccount(), getTerminalDeviceId(), cloudDeviceApiRequest);

Assertions.assertNotNull(response);
Assertions.assertEquals("ok", response.getResult());
}

@Disabled("Enable when you want to test with the Terminal")
@Test
public void sendEncryptedSync() throws Exception {

CloudDeviceApiRequest cloudDeviceApiRequest =
createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());

EncryptionCredentialDetails encryptionCredentialDetails =
new EncryptionCredentialDetails()
.adyenCryptoVersion(1)
.keyIdentifier(getTerminalDeviceKeyIdentifier())
.keyVersion(1)
.passphrase(getTerminalDevicePassphrase());

EncryptedCloudDeviceApi encryptedCloudDeviceApi =
new EncryptedCloudDeviceApi(getClient(), encryptionCredentialDetails);
Comment thread
gcatanese marked this conversation as resolved.

var response =
encryptedCloudDeviceApi.sync(
getMerchantAccount(), getTerminalDeviceId(), cloudDeviceApiRequest);

Assertions.assertNotNull(response);
Assertions.assertNotNull(response.getSaleToPOIResponse());
Assertions.assertEquals(getTerminalDeviceId(), response.getSaleToPOIResponse().getMessageHeader().getPOIID());
}

@Disabled("Enable when you want to test with the Terminal")
@Test
public void getConnectedDevices() throws Exception {

CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(getClient());

var response = cloudDeviceApi.getConnectedDevices(getMerchantAccount());

Assertions.assertNotNull(response);
Assertions.assertNotNull(response.getUniqueDeviceIds());
Assertions.assertTrue(response.getUniqueDeviceIds().contains(getTerminalDeviceId()));
}

@Disabled("Enable when you want to test with the Terminal")
@Test
public void sendEncryptedAsync() throws Exception {

CloudDeviceApiRequest cloudDeviceApiRequest =
createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());

EncryptionCredentialDetails encryptionCredentialDetails =
new EncryptionCredentialDetails()
.adyenCryptoVersion(1)
.keyIdentifier(getTerminalDeviceKeyIdentifier())
.keyVersion(1)
.passphrase(getTerminalDevicePassphrase());

EncryptedCloudDeviceApi encryptedCloudDeviceApi =
new EncryptedCloudDeviceApi(getClient(), encryptionCredentialDetails);

var response =
encryptedCloudDeviceApi.async(
getMerchantAccount(), getTerminalDeviceId(), cloudDeviceApiRequest);

Assertions.assertNotNull(response);
Assertions.assertEquals("ok", response.getResult());
}

protected CloudDeviceApiRequest createCloudDeviceAPIPaymentRequest(String deviceId) {
SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest();

var randomId = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 10);

MessageHeader messageHeader = new MessageHeader();
messageHeader.setProtocolVersion("3.0");
messageHeader.setMessageClass(MessageClass.SERVICE);
messageHeader.setMessageCategory(MessageCategory.PAYMENT);
messageHeader.setMessageType(MessageType.REQUEST);
messageHeader.setSaleID(randomId);
messageHeader.setServiceID(randomId);
messageHeader.setPOIID(deviceId);

saleToPOIRequest.setMessageHeader(messageHeader);

PaymentRequest paymentRequest = new PaymentRequest();

SaleData saleData = new SaleData();
TransactionIDType transactionIdentification = new TransactionIDType();
transactionIdentification.setTransactionID(randomId);
OffsetDateTime timestamp = OffsetDateTime.now(ZoneOffset.UTC);
transactionIdentification.setTimeStamp(timestamp);
saleData.setSaleTransactionID(transactionIdentification);

PaymentTransaction paymentTransaction = new PaymentTransaction();
AmountsReq amountsReq = new AmountsReq();
amountsReq.setCurrency("EUR");
amountsReq.setRequestedAmount(BigDecimal.TEN);
paymentTransaction.setAmountsReq(amountsReq);

paymentRequest.setSaleData(saleData);
paymentRequest.setPaymentTransaction(paymentTransaction);

saleToPOIRequest.setPaymentRequest(paymentRequest);

CloudDeviceApiRequest cloudDeviceApiRequest = new CloudDeviceApiRequest();
cloudDeviceApiRequest.setSaleToPOIRequest(saleToPOIRequest);

return cloudDeviceApiRequest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.adyen.enums.Region;
import com.adyen.model.clouddevice.*;
import com.adyen.model.tapi.*;
import com.fasterxml.jackson.databind.JsonNode;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
Expand Down Expand Up @@ -224,6 +225,66 @@ public void getDeviceStatus() throws Exception {
null);
}

@Test
public void cloudDeviceApiRequestSerialisesMessageHeader() throws Exception {
CloudDeviceApiRequest request = createCloudDeviceAPIPaymentRequest();

String json = request.toJson();
JsonNode root = JSON.getMapper().readTree(json);
JsonNode messageHeader = root.path("SaleToPOIRequest").path("MessageHeader");

Assertions.assertEquals("3.0", messageHeader.path("ProtocolVersion").asText());
Assertions.assertEquals("Service", messageHeader.path("MessageClass").asText());
Assertions.assertEquals("Payment", messageHeader.path("MessageCategory").asText());
Assertions.assertEquals("Request", messageHeader.path("MessageType").asText());
Assertions.assertEquals("001", messageHeader.path("SaleID").asText());
Assertions.assertEquals("001", messageHeader.path("ServiceID").asText());
Assertions.assertEquals("P400Plus-123456789", messageHeader.path("POIID").asText());
}

@Test
public void cloudDeviceApiRequestSerialisesPaymentRequest() throws Exception {
CloudDeviceApiRequest request = createCloudDeviceAPIPaymentRequest();

String json = request.toJson();
JsonNode root = JSON.getMapper().readTree(json);
JsonNode amountsReq =
root.path("SaleToPOIRequest")
.path("PaymentRequest")
.path("PaymentTransaction")
.path("AmountsReq");

Assertions.assertEquals("EUR", amountsReq.path("Currency").asText());
Assertions.assertEquals(
BigDecimal.ONE, amountsReq.path("RequestedAmount").decimalValue().stripTrailingZeros());
}

@Test
public void cloudDeviceApiRequestSerialisesTransactionId() throws Exception {
CloudDeviceApiRequest request = createCloudDeviceAPIPaymentRequest();

String json = request.toJson();
JsonNode root = JSON.getMapper().readTree(json);
JsonNode saleTransactionID =
root.path("SaleToPOIRequest")
.path("PaymentRequest")
.path("SaleData")
.path("SaleTransactionID");

Assertions.assertEquals("001", saleTransactionID.path("TransactionID").asText());
Assertions.assertFalse(saleTransactionID.path("TimeStamp").isMissingNode());
}

@Test
public void cloudDeviceApiRequestRoundTrip() throws Exception {
CloudDeviceApiRequest original = createCloudDeviceAPIPaymentRequest();

String json = original.toJson();
CloudDeviceApiRequest deserialised = CloudDeviceApiRequest.fromJson(json);

Assertions.assertEquals(original, deserialised);
}

protected CloudDeviceApiRequest createCloudDeviceAPIPaymentRequest() {
SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest();

Expand Down
Loading
Loading