diff --git a/cloud/start-server.sh b/cloud/start-server.sh index dc2a203c..431a9c9c 100644 --- a/cloud/start-server.sh +++ b/cloud/start-server.sh @@ -17,5 +17,7 @@ docker run -d --rm \ -e POSTGRES_HOST=$POSTGRES_HOST \ -e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \ -e SERVER_SERVLET_CONTEXT_PATH=$SERVER_SERVLET_CONTEXT_PATH \ + -e MAILGUN_APIKEY=$MAILGUN_APIKEY \ + -e SECURITY_KEY=$SECURITY_KEY \ -e BUILD=ghcr.io/ricardo-campos-org/react-typescript-todolist/server:$PR \ ghcr.io/ricardo-campos-org/react-typescript-todolist/server:$PR diff --git a/server/pom.xml b/server/pom.xml index 091fe8f8..638eb34b 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -100,10 +100,6 @@ org.springframework.boot spring-boot-starter-security - - org.springframework.boot - spring-boot-starter-mail - diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java b/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java index 4d44d230..ae691bb8 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java @@ -1,36 +1,29 @@ package br.com.tasknoteapp.server.service; import br.com.tasknoteapp.server.entity.UserEntity; -import br.com.tasknoteapp.server.exception.MailServiceException; import br.com.tasknoteapp.server.templates.MailgunTemplate; import br.com.tasknoteapp.server.templates.MailgunTemplateEmailChanged; import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwd; import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwdConfirm; import br.com.tasknoteapp.server.templates.MailgunTemplateSignUp; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Base64; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.RestClient; /** This service handles email messages for Mailgun. */ @Service public class MailgunEmailService { private static final Logger logger = LoggerFactory.getLogger(MailgunEmailService.class.getName()); - private final RestTemplate restTemplate; + private final RestClient restClient; private final String targetEnv; private final String domain; private String senderEmail; @@ -42,23 +35,37 @@ public class MailgunEmailService { * @param domain The domain to send email from. * @param sender The from option. * @param targetEnv The environment. - * @param templateBuilder The template builder. + * @param restClientBuilder The rest client builder. */ public MailgunEmailService( @Value("${mailgun.api-key}") String apiKey, @Value("${mailgun.domain}") String domain, @Value("${mailgun.sender-email}") String sender, @Value("${br.com.tasknote.server.target-env}") String targetEnv, - RestTemplateBuilder templateBuilder) { + RestClient.Builder restClientBuilder) { this.domain = domain; this.senderEmail = sender; this.targetEnv = targetEnv; - this.restTemplate = - templateBuilder - .connectTimeout(Duration.ofSeconds(5)) - .readTimeout(Duration.ofSeconds(10)) - .defaultHeader(HttpHeaders.AUTHORIZATION, basicAuth("api", apiKey)) - .build(); + + if (apiKey != null && apiKey.length() > 6) { + logger.info( + "Mailgun API Key loaded: {}...{}", + apiKey.substring(0, 3), + apiKey.substring(apiKey.length() - 3)); + } else { + logger.warn("Mailgun API Key is missing or too short!"); + } + + this.restClient = restClientBuilder + .baseUrl("https://api.mailgun.net/v3/" + domain) + .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> { + logger.error( + "Mailgun API Error: {} {}", + response.getStatusCode(), + response.getStatusText()); + }) + .defaultHeaders(headers -> headers.setBasicAuth("api", apiKey)) + .build(); } /** @@ -141,16 +148,11 @@ public void sendEmailChangedNotification(UserEntity user, String oldEmail) { * * @param to The target email address. * @param subject The message subject. - * @param textBody The message text body to be displayed. - * @param htmlBody The message html body to be rendered. + * @param template The mailgun template. */ private void sendEmail(String to, String subject, MailgunTemplate template) { - String url = "https://api.mailgun.net/v3/" + domain + "/messages"; String from = "TaskNote App <" + senderEmail + ">"; - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - MultiValueMap mailData = new LinkedMultiValueMap<>(); mailData.add("from", from); mailData.add("to", to); @@ -164,26 +166,22 @@ private void sendEmail(String to, String subject, MailgunTemplate template) { logger.info("JSON template variables: {}", template.getVariableValuesJson()); } - HttpEntity> request = new HttpEntity<>(mailData, headers); - try { - ResponseEntity response = restTemplate.postForEntity(url, request, String.class); - - if (!response.getStatusCode().is2xxSuccessful()) { - throw new MailServiceException("Failed to send email: " + response.getStatusCode()); - } + restClient.post() + .uri("/messages") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(mailData) + .retrieve() + .toBodilessEntity(); logger.info("Email message send successfully."); } catch (HttpClientErrorException ex) { - logger.error("Unable to send email: {} - {}", ex.getMessage(), ex.getCause()); + logger.error("Unable to send email: {} - {}", ex.getMessage(), ex.getStatusCode()); + } catch (Exception ex) { + logger.error("Unexpected error sending email: {}", ex.getMessage()); } } - private String basicAuth(String username, String password) { - String auth = username + ":" + password; - return "Basic " + Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); - } - private String getBaseUrl() { if ("development".equals(targetEnv) || Objects.isNull(targetEnv)) { return "http://localhost:5000"; diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java index 9afb562c..0c58e88c 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java @@ -2,7 +2,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -13,34 +12,43 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.http.HttpStatusCode; -import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.RestClient; -/** Test class generated by Copilot. I had to tweak it, to make it work. */ +/** Test class for MailgunEmailService using RestClient. */ @ExtendWith(MockitoExtension.class) class MailgunEmailServiceTest { - @Mock private RestTemplate restTemplate; - @Mock private RestTemplateBuilder restTemplateBuilder; + @Mock private RestClient restClient; + @Mock private RestClient.Builder restClientBuilder; + @Mock private RestClient.RequestBodyUriSpec requestBodyUriSpec; + @Mock private RestClient.RequestBodySpec requestBodySpec; + @Mock private RestClient.ResponseSpec responseSpec; private MailgunEmailService mailgunEmailService; @BeforeEach void setUp() { - when(restTemplateBuilder.connectTimeout(any())).thenReturn(restTemplateBuilder); - when(restTemplateBuilder.readTimeout(any())).thenReturn(restTemplateBuilder); - when(restTemplateBuilder.defaultHeader(any(), any())).thenReturn(restTemplateBuilder); - when(restTemplateBuilder.build()).thenReturn(restTemplate); + when(restClientBuilder.baseUrl(anyString())).thenReturn(restClientBuilder); + when(restClientBuilder.defaultStatusHandler(any(), any())).thenReturn(restClientBuilder); + when(restClientBuilder.defaultHeaders(any())).thenReturn(restClientBuilder); + when(restClientBuilder.build()).thenReturn(restClient); String apiKey = "abx123"; String domain = "domain.com"; String sender = "no-reply@domain.com"; String target = "development"; mailgunEmailService = - new MailgunEmailService(apiKey, domain, sender, target, restTemplateBuilder); + new MailgunEmailService(apiKey, domain, sender, target, restClientBuilder); + } + + private void setupMockChain() { + when(restClient.post()).thenReturn(requestBodyUriSpec); + when(requestBodyUriSpec.uri(anyString())).thenReturn(requestBodySpec); + when(requestBodySpec.contentType(any())).thenReturn(requestBodySpec); + when(requestBodySpec.body(any())).thenReturn(requestBodySpec); + when(requestBodySpec.retrieve()).thenReturn(responseSpec); } @Test @@ -49,12 +57,11 @@ void testSendResetPassword() { user.setEmail("test@example.com"); user.setResetToken("reset-token"); - when(restTemplate.postForEntity(anyString(), any(), eq(String.class))) - .thenReturn(ResponseEntity.ok("Success")); + setupMockChain(); mailgunEmailService.sendResetPassword(user); - verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class)); + verify(restClient, times(1)).post(); } @Test @@ -62,12 +69,11 @@ void testSendPasswordResetConfirmation() { UserEntity user = new UserEntity(); user.setEmail("test@example.com"); - when(restTemplate.postForEntity(anyString(), any(), eq(String.class))) - .thenReturn(ResponseEntity.ok("Success")); + setupMockChain(); mailgunEmailService.sendPasswordResetConfirmation(user); - verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class)); + verify(restClient, times(1)).post(); } @Test @@ -76,12 +82,11 @@ void testSendNewUser() { user.setEmail("test@example.com"); user.setEmailUuid(java.util.UUID.randomUUID()); - when(restTemplate.postForEntity(anyString(), any(), eq(String.class))) - .thenReturn(ResponseEntity.ok("Success")); + setupMockChain(); mailgunEmailService.sendNewUser(user); - verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class)); + verify(restClient, times(1)).post(); } @Test @@ -90,12 +95,13 @@ void testSendEmailHandlesHttpClientErrorException() { user.setEmail("test@example.com"); user.setResetToken("reset-token"); - when(restTemplate.postForEntity(anyString(), any(), eq(String.class))) + setupMockChain(); + when(responseSpec.toBodilessEntity()) .thenThrow(new HttpClientErrorException(HttpStatusCode.valueOf(400))); mailgunEmailService.sendResetPassword(user); - verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class)); + verify(restClient, times(1)).post(); } @Test @@ -103,13 +109,12 @@ void testSendEmailChanged() { UserEntity user = new UserEntity(); user.setEmail("test@example.com"); - when(restTemplate.postForEntity(anyString(), any(), eq(String.class))) - .thenReturn(ResponseEntity.ok("Success")); + setupMockChain(); String oldEmail = "old@example.com"; mailgunEmailService.sendEmailChangedNotification(user, oldEmail); - verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class)); + verify(restClient, times(1)).post(); } }