| 1 | package com.edtech.service; | |
| 2 | ||
| 3 | import java.io.IOException; | |
| 4 | import java.net.URI; | |
| 5 | import java.net.http.HttpClient; | |
| 6 | import java.net.http.HttpRequest; | |
| 7 | import java.net.http.HttpResponse; | |
| 8 | import java.nio.charset.StandardCharsets; | |
| 9 | import org.slf4j.Logger; | |
| 10 | import org.slf4j.LoggerFactory; | |
| 11 | import org.springframework.beans.factory.annotation.Autowired; | |
| 12 | import org.springframework.beans.factory.annotation.Value; | |
| 13 | import org.springframework.mail.MailException; | |
| 14 | import org.springframework.mail.SimpleMailMessage; | |
| 15 | import org.springframework.mail.javamail.JavaMailSender; | |
| 16 | import org.springframework.scheduling.annotation.Async; | |
| 17 | import org.springframework.stereotype.Service; | |
| 18 | ||
| 19 | /** Sends account emails through the Resend HTTPS API in production and SMTP locally. */ | |
| 20 | @Service | |
| 21 | public class EmailService { | |
| 22 | ||
| 23 | private static final Logger logger = LoggerFactory.getLogger(EmailService.class); | |
| 24 | private static final URI RESEND_EMAILS_URI = URI.create("https://api.resend.com/emails"); | |
| 25 | ||
| 26 | @Autowired private JavaMailSender mailSender; | |
| 27 | ||
| 28 | @Value("${SMTP_FROM:noreply@edtechacademic.com.br}") | |
| 29 | private String fromAddress; | |
| 30 | ||
| 31 | @Value("${SMTP_PASSWORD:}") | |
| 32 | private String resendApiKey; | |
| 33 | ||
| 34 | /** Sends the password recovery code. */ | |
| 35 | @Async | |
| 36 | public void sendRecoveryEmail(String toEmail, String otpCode) { | |
| 37 |
1
1. sendRecoveryEmail : removed call to com/edtech/service/EmailService::sendEmail → KILLED |
sendEmail( |
| 38 | toEmail, | |
| 39 | "Código de Recuperação de Senha - EdTech", | |
| 40 | "Olá,\n\n" | |
| 41 | + "Você solicitou a recuperação da sua senha na EdTech.\n" | |
| 42 | + "Seu código de segurança (OTP) de 6 dígitos é: " | |
| 43 | + otpCode | |
| 44 | + "\n\n" | |
| 45 | + "Este código é válido por 15 minutos.\n" | |
| 46 | + "Se você não solicitou isso, ignore este e-mail.\n\n" | |
| 47 | + "Equipe EdTech"); | |
| 48 | } | |
| 49 | ||
| 50 | /** Sends the registration verification code. */ | |
| 51 | @Async | |
| 52 | public void sendVerificationEmail(String toEmail, String otpCode) { | |
| 53 |
1
1. sendVerificationEmail : removed call to com/edtech/service/EmailService::sendEmail → KILLED |
sendEmail( |
| 54 | toEmail, | |
| 55 | "Código de Verificação de Conta - EdTech", | |
| 56 | "Olá,\n\n" | |
| 57 | + "Bem-vindo à EdTech!\n" | |
| 58 | + "Seu código de verificação (OTP) de 6 dígitos é: " | |
| 59 | + otpCode | |
| 60 | + "\n\n" | |
| 61 | + "Este código é válido por 15 minutos.\n" | |
| 62 | + "Se você não solicitou isso, ignore este e-mail.\n\n" | |
| 63 | + "Equipe EdTech"); | |
| 64 | } | |
| 65 | ||
| 66 | private void sendEmail(String toEmail, String subject, String text) { | |
| 67 |
1
1. sendEmail : negated conditional → KILLED |
String apiKey = resendApiKey == null ? "" : resendApiKey.trim(); |
| 68 |
1
1. sendEmail : negated conditional → KILLED |
if (apiKey.startsWith("re_")) { |
| 69 |
1
1. sendEmail : removed call to com/edtech/service/EmailService::sendWithResendApi → NO_COVERAGE |
sendWithResendApi(apiKey, toEmail, subject, text); |
| 70 | return; | |
| 71 | } | |
| 72 | ||
| 73 | SimpleMailMessage message = new SimpleMailMessage(); | |
| 74 |
1
1. sendEmail : removed call to org/springframework/mail/SimpleMailMessage::setFrom → KILLED |
message.setFrom(resolveFromAddress()); |
| 75 |
1
1. sendEmail : removed call to org/springframework/mail/SimpleMailMessage::setTo → KILLED |
message.setTo(toEmail); |
| 76 |
1
1. sendEmail : removed call to org/springframework/mail/SimpleMailMessage::setSubject → KILLED |
message.setSubject(subject); |
| 77 |
1
1. sendEmail : removed call to org/springframework/mail/SimpleMailMessage::setText → KILLED |
message.setText(text); |
| 78 | try { | |
| 79 |
1
1. sendEmail : removed call to org/springframework/mail/javamail/JavaMailSender::send → KILLED |
mailSender.send(message); |
| 80 | logger.info("Email dispatched through SMTP to {}.", toEmail); | |
| 81 | } catch (MailException ex) { | |
| 82 | logger.warn("SMTP email delivery failed for {}.", toEmail, ex); | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | private void sendWithResendApi(String apiKey, String toEmail, String subject, String text) { | |
| 87 | String payload = | |
| 88 | "{\"from\":\"" | |
| 89 | + escapeJson(resolveFromAddress()) | |
| 90 | + "\",\"to\":[\"" | |
| 91 | + escapeJson(toEmail) | |
| 92 | + "\"],\"subject\":\"" | |
| 93 | + escapeJson(subject) | |
| 94 | + "\",\"text\":\"" | |
| 95 | + escapeJson(text) | |
| 96 | + "\"}"; | |
| 97 | HttpRequest request = | |
| 98 | HttpRequest.newBuilder(RESEND_EMAILS_URI) | |
| 99 | .header("Authorization", "Bearer " + apiKey) | |
| 100 | .header("Content-Type", "application/json") | |
| 101 | .POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8)) | |
| 102 | .build(); | |
| 103 | try { | |
| 104 | HttpResponse<String> response = | |
| 105 | HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); | |
| 106 |
4
1. sendWithResendApi : changed conditional boundary → NO_COVERAGE 2. sendWithResendApi : negated conditional → NO_COVERAGE 3. sendWithResendApi : negated conditional → NO_COVERAGE 4. sendWithResendApi : changed conditional boundary → NO_COVERAGE |
if (response.statusCode() >= 200 && response.statusCode() < 300) { |
| 107 | logger.info("Email dispatched through Resend API to {}.", toEmail); | |
| 108 | } else { | |
| 109 | logger.warn( | |
| 110 | "Resend API rejected email for {} with HTTP status {}.", | |
| 111 | toEmail, | |
| 112 | response.statusCode()); | |
| 113 | } | |
| 114 | } catch (IOException ex) { | |
| 115 | logger.warn("Resend API delivery failed for {}.", toEmail, ex); | |
| 116 | } catch (InterruptedException ex) { | |
| 117 |
1
1. sendWithResendApi : removed call to java/lang/Thread::interrupt → NO_COVERAGE |
Thread.currentThread().interrupt(); |
| 118 | logger.warn("Resend API delivery interrupted for {}.", toEmail, ex); | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | private String escapeJson(String value) { | |
| 123 |
1
1. escapeJson : replaced return value with "" for com/edtech/service/EmailService::escapeJson → NO_COVERAGE |
return value |
| 124 | .replace("\\", "\\\\") | |
| 125 | .replace("\"", "\\\"") | |
| 126 | .replace("\n", "\\n") | |
| 127 | .replace("\r", "\\r"); | |
| 128 | } | |
| 129 | ||
| 130 | private String resolveFromAddress() { | |
| 131 |
3
1. resolveFromAddress : replaced return value with "" for com/edtech/service/EmailService::resolveFromAddress → KILLED 2. resolveFromAddress : negated conditional → KILLED 3. resolveFromAddress : negated conditional → KILLED |
return fromAddress == null || fromAddress.isBlank() |
| 132 | ? "noreply@edtechacademic.com.br" | |
| 133 | : fromAddress; | |
| 134 | } | |
| 135 | } | |
Mutations | ||
| 37 |
1.1 |
|
| 53 |
1.1 |
|
| 67 |
1.1 |
|
| 68 |
1.1 |
|
| 69 |
1.1 |
|
| 74 |
1.1 |
|
| 75 |
1.1 |
|
| 76 |
1.1 |
|
| 77 |
1.1 |
|
| 79 |
1.1 |
|
| 106 |
1.1 2.2 3.3 4.4 |
|
| 117 |
1.1 |
|
| 123 |
1.1 |
|
| 131 |
1.1 2.2 3.3 |