diff --git a/spring-web-modules/spring-resttemplate-3/pom.xml b/spring-web-modules/spring-resttemplate-3/pom.xml index 86b0dccb2e4f..1174107a6219 100644 --- a/spring-web-modules/spring-resttemplate-3/pom.xml +++ b/spring-web-modules/spring-resttemplate-3/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-resttemplate-3 0.1-SNAPSHOT @@ -63,6 +63,12 @@ commons-io ${commons-io.version} + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + diff --git a/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/config/RestTemplateConfig.java b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/config/RestTemplateConfig.java new file mode 100644 index 000000000000..7d439bf0c2be --- /dev/null +++ b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package com.baeldung.xmlpost.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/model/PaymentRequest.java b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/model/PaymentRequest.java new file mode 100644 index 000000000000..5283fa71f2eb --- /dev/null +++ b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/model/PaymentRequest.java @@ -0,0 +1,62 @@ +package com.baeldung.xmlpost.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "PaymentRequest") +public class PaymentRequest { + + @JacksonXmlProperty(localName = "transactionId") + private String transactionId; + + @JacksonXmlProperty(localName = "amount") + private Double amount; + + @JacksonXmlProperty(localName = "currency") + private String currency; + + @JacksonXmlProperty(localName = "recipient") + private String recipient; + + public PaymentRequest() { + } + + public PaymentRequest(String transactionId, Double amount, String currency, String recipient) { + this.transactionId = transactionId; + this.amount = amount; + this.currency = currency; + this.recipient = recipient; + } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public String getRecipient() { + return recipient; + } + + public void setRecipient(String recipient) { + this.recipient = recipient; + } +} diff --git a/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/model/PaymentResponse.java b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/model/PaymentResponse.java new file mode 100644 index 000000000000..317875672c45 --- /dev/null +++ b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/model/PaymentResponse.java @@ -0,0 +1,50 @@ +package com.baeldung.xmlpost.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "PaymentResponse") +public class PaymentResponse { + + @JacksonXmlProperty(localName = "status") + private String status; + + @JacksonXmlProperty(localName = "message") + private String message; + + @JacksonXmlProperty(localName = "referenceNumber") + private String referenceNumber; + + public PaymentResponse() { + } + + public PaymentResponse(String status, String message, String referenceNumber) { + this.status = status; + this.message = message; + this.referenceNumber = referenceNumber; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getReferenceNumber() { + return referenceNumber; + } + + public void setReferenceNumber(String referenceNumber) { + this.referenceNumber = referenceNumber; + } +} diff --git a/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/service/PaymentService.java b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/service/PaymentService.java new file mode 100644 index 000000000000..8ba92d242cc0 --- /dev/null +++ b/spring-web-modules/spring-resttemplate-3/src/main/java/com/baeldung/xmlpost/service/PaymentService.java @@ -0,0 +1,39 @@ +package com.baeldung.xmlpost.service; + +import com.baeldung.xmlpost.model.PaymentRequest; +import com.baeldung.xmlpost.model.PaymentResponse; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; + +@Service +public class PaymentService { + private final RestTemplate restTemplate; + + public PaymentService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public PaymentResponse processPayment(PaymentRequest request, String paymentUrl) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML)); + + HttpEntity entity = new HttpEntity<>(request, headers); + + ResponseEntity response = + restTemplate.postForEntity(paymentUrl, entity, PaymentResponse.class); + + return response.getBody(); + } catch (Exception ex) { + throw new RuntimeException("Payment processing failed: " + ex.getMessage(), ex); + } + } +} + diff --git a/spring-web-modules/spring-resttemplate-3/src/test/java/com/baeldung/xmlpost/service/PaymentServiceUnitTest.java b/spring-web-modules/spring-resttemplate-3/src/test/java/com/baeldung/xmlpost/service/PaymentServiceUnitTest.java new file mode 100644 index 000000000000..18ade69ff92e --- /dev/null +++ b/spring-web-modules/spring-resttemplate-3/src/test/java/com/baeldung/xmlpost/service/PaymentServiceUnitTest.java @@ -0,0 +1,92 @@ +package com.baeldung.xmlpost.service; + +import com.baeldung.xmlpost.model.PaymentRequest; +import com.baeldung.xmlpost.model.PaymentResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PaymentServiceUnitTest { + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private PaymentService paymentService; + + private final String testUrl = "http://mock-payment-service"; + + @Test + void givenValidPaymentRequest_whenProcessPayment_thenReturnSuccessfulResponse() { + PaymentRequest request = new PaymentRequest("TXN001", 100.50, "USD", "Jane Doe"); + PaymentResponse expectedResponse = new PaymentResponse( + "SUCCESS", "Payment processed successfully", "REF12345" + ); + + ResponseEntity mockResponse = + new ResponseEntity<>(expectedResponse, HttpStatus.OK); + + when(restTemplate.postForEntity(eq(testUrl), any(HttpEntity.class), eq(PaymentResponse.class))) + .thenReturn(mockResponse); + + PaymentResponse actualResponse = paymentService.processPayment(request, testUrl); + + assertNotNull(actualResponse); + assertEquals("SUCCESS", actualResponse.getStatus()); + assertEquals("REF12345", actualResponse.getReferenceNumber()); + assertEquals("Payment processed successfully", actualResponse.getMessage()); + + verify(restTemplate).postForEntity(eq(testUrl), any(HttpEntity.class), eq(PaymentResponse.class)); + } + + @Test + void givenRemoteServiceReturnsBadRequest_whenProcessPayment_thenThrowMeaningfulException() { + PaymentRequest request = new PaymentRequest("TXN002", 200.0, "EUR", "John Smith"); + + when(restTemplate.postForEntity(eq(testUrl), any(HttpEntity.class), eq(PaymentResponse.class))) + .thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Invalid amount")); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> paymentService.processPayment(request, testUrl)); + + assertTrue(exception.getMessage().contains("Payment processing failed")); + assertTrue(exception.getMessage().contains("Invalid amount")); + } + + @Test + void givenXmlRequest_whenProcessPayment_thenSetCorrectXmlHttpHeaders() { + PaymentRequest request = new PaymentRequest("TXN004", 300.0, "CAD", "Bob Wilson"); + PaymentResponse expectedResponse = new PaymentResponse("SUCCESS", "OK", "REF67890"); + + when(restTemplate.postForEntity(eq(testUrl), any(HttpEntity.class), eq(PaymentResponse.class))) + .thenReturn(new ResponseEntity<>(expectedResponse, HttpStatus.OK)); + + paymentService.processPayment(request, testUrl); + + verify(restTemplate).postForEntity( + eq(testUrl), + argThat((HttpEntity entity) -> { + boolean hasXmlContentType = entity.getHeaders().getContentType() + .includes(MediaType.APPLICATION_XML); + boolean acceptsXml = entity.getHeaders().getAccept() + .contains(MediaType.APPLICATION_XML); + return hasXmlContentType && acceptsXml; + }), + eq(PaymentResponse.class) + ); + } +} \ No newline at end of file