feat(validation): add request and domain validation for email use case

This commit is contained in:
Pablo de la Torre Jamardo 2025-09-14 14:46:23 +02:00
parent 421d160c12
commit fff9362ea8
6 changed files with 71 additions and 27 deletions

View File

@ -5,6 +5,7 @@ import com.pablotj.restemailbridge.application.port.in.EmailDefaultConfigPort;
import com.pablotj.restemailbridge.application.port.out.EmailPort; import com.pablotj.restemailbridge.application.port.out.EmailPort;
import com.pablotj.restemailbridge.domain.model.Email; import com.pablotj.restemailbridge.domain.model.Email;
import com.pablotj.restemailbridge.domain.repository.EmailRepository; import com.pablotj.restemailbridge.domain.repository.EmailRepository;
import com.pablotj.restemailbridge.domain.service.EmailValidatorService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -18,20 +19,13 @@ public class SendEmailUseCase {
private static final Logger log = LoggerFactory.getLogger(SendEmailUseCase.class); private static final Logger log = LoggerFactory.getLogger(SendEmailUseCase.class);
private final EmailValidatorService emailValidatorService;
private final EmailDefaultConfigPort emailConfigurationPort; private final EmailDefaultConfigPort emailConfigurationPort;
private final EmailPort emailService; private final EmailPort emailService;
private final EmailRepository emailRepository; private final EmailRepository emailRepository;
/** public SendEmailUseCase(EmailValidatorService emailValidatorService, EmailDefaultConfigPort emailConfigurationPort, EmailPort emailService, EmailRepository emailRepository) {
* Constructor injecting required ports. this.emailValidatorService = emailValidatorService;
*
* @param emailConfigurationPort Port to retrieve configuration
* @param emailService Service to send emails
* @param emailRepository Repository to persist emails
*/
public SendEmailUseCase(EmailDefaultConfigPort emailConfigurationPort,
EmailPort emailService,
EmailRepository emailRepository) {
this.emailConfigurationPort = emailConfigurationPort; this.emailConfigurationPort = emailConfigurationPort;
this.emailService = emailService; this.emailService = emailService;
this.emailRepository = emailRepository; this.emailRepository = emailRepository;
@ -44,16 +38,19 @@ public class SendEmailUseCase {
*/ */
public void handle(EmailDTO emailDTO) { public void handle(EmailDTO emailDTO) {
String to = emailConfigurationPort.getDefaultRecipient(); String to = emailConfigurationPort.getDefaultRecipient();
Email email = Email.builder()
.from(emailDTO.from())
.to(to)
.subject(emailDTO.subject())
.body(emailDTO.body())
.build();
emailValidatorService.validate(email);
log.info("Sending email from {} to {}", emailDTO.from(), to); log.info("Sending email from {} to {}", emailDTO.from(), to);
Email email = emailService.sendEmail( email = emailService.sendEmail(email);
Email.builder()
.from(emailDTO.from())
.to(to)
.subject(emailDTO.subject())
.body(emailDTO.body())
.build()
);
emailRepository.save(email); emailRepository.save(email);
log.info("Email successfully sent and persisted to repository for recipient {}", to); log.info("Email successfully sent and persisted to repository for recipient {}", to);

View File

@ -0,0 +1,22 @@
package com.pablotj.restemailbridge.domain.service;
import com.pablotj.restemailbridge.domain.model.Email;
public class EmailValidatorService {
/**
* Validates business rules for Email.
*/
public void validate(Email email) {
if (email == null) throw new IllegalArgumentException("Email cannot be null");
if (email.getTo() == null || !email.getTo().matches(".+@.+\\..+"))
throw new IllegalArgumentException("Recipient email is invalid");
if (email.getFrom() == null || email.getFrom().isBlank())
throw new IllegalArgumentException("Sender email is required");
if (email.getSubject() == null || email.getSubject().isBlank())
throw new IllegalArgumentException("Subject is required");
if (email.getBody() == null || email.getBody().isBlank())
throw new IllegalArgumentException("Body is required");
}
}

View File

@ -4,6 +4,7 @@ import com.pablotj.restemailbridge.application.port.in.EmailDefaultConfigPort;
import com.pablotj.restemailbridge.application.port.out.EmailPort; import com.pablotj.restemailbridge.application.port.out.EmailPort;
import com.pablotj.restemailbridge.application.usecase.SendEmailUseCase; import com.pablotj.restemailbridge.application.usecase.SendEmailUseCase;
import com.pablotj.restemailbridge.domain.repository.EmailRepository; import com.pablotj.restemailbridge.domain.repository.EmailRepository;
import com.pablotj.restemailbridge.domain.service.EmailValidatorService;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -12,6 +13,6 @@ public class UseCaseConfig {
@Bean @Bean
public SendEmailUseCase sendEmailUseCase(EmailDefaultConfigPort emailConfigurationPort, EmailPort emailService, EmailRepository emailRepository) { public SendEmailUseCase sendEmailUseCase(EmailDefaultConfigPort emailConfigurationPort, EmailPort emailService, EmailRepository emailRepository) {
return new SendEmailUseCase(emailConfigurationPort, emailService, emailRepository); return new SendEmailUseCase(new EmailValidatorService(), emailConfigurationPort, emailService, emailRepository);
} }
} }

View File

@ -19,15 +19,15 @@ public class MailJpa {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(nullable = false) @Column(length = 100, nullable = false)
private String sender; private String sender;
@Column(nullable = false) @Column(length = 100, nullable = false)
private String recipient; private String recipient;
@Column(nullable = false) @Column(length = 50, nullable = false)
private String subjet; private String subjet;
@Column(nullable = false) @Column(length = 40000, nullable = false)
private String body; private String body;
} }

View File

@ -0,0 +1,20 @@
package com.pablotj.restemailbridge.infrastructure.rest;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}

View File

@ -1,8 +1,12 @@
package com.pablotj.restemailbridge.infrastructure.rest.dto; package com.pablotj.restemailbridge.infrastructure.rest.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.Length;
public record SendMailRequest( public record SendMailRequest(
String from, @NotBlank @Email @Length(min = 4, max = 100) String from,
String subject, @NotBlank @Length(min=1, max = 30) String subject,
String body @NotBlank @Length(min=1, max = 4000) String body
) { ) {
} }