diff --git a/application/pom.xml b/application/pom.xml
index d4a0db7..4367d24 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -23,5 +23,25 @@
2.0.13
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 5.14.2
+ test
+
+
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
\ No newline at end of file
diff --git a/application/src/test/java/com/pablotj/restemailbridge/application/usecase/SendEmailUseCaseTest.java b/application/src/test/java/com/pablotj/restemailbridge/application/usecase/SendEmailUseCaseTest.java
new file mode 100644
index 0000000..70f5532
--- /dev/null
+++ b/application/src/test/java/com/pablotj/restemailbridge/application/usecase/SendEmailUseCaseTest.java
@@ -0,0 +1,129 @@
+package com.pablotj.restemailbridge.application.usecase;
+
+import com.pablotj.restemailbridge.application.dto.EmailDTO;
+import com.pablotj.restemailbridge.application.port.in.EmailDefaultConfigPort;
+import com.pablotj.restemailbridge.application.port.out.EmailPort;
+import com.pablotj.restemailbridge.domain.model.Email;
+import com.pablotj.restemailbridge.domain.model.EmailStatus;
+import com.pablotj.restemailbridge.domain.repository.EmailRepository;
+import com.pablotj.restemailbridge.domain.service.EmailValidatorService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class SendEmailUseCaseTest {
+
+ private EmailValidatorService emailValidatorService;
+ private EmailPort emailPort;
+ private EmailRepository emailRepository;
+
+ private SendEmailUseCase useCase;
+
+ @BeforeEach
+ void setUp() {
+ EmailDefaultConfigPort emailDefaultConfigPort = mock(EmailDefaultConfigPort.class);
+
+ emailValidatorService = mock(EmailValidatorService.class);
+ emailPort = mock(EmailPort.class);
+ emailRepository = mock(EmailRepository.class);
+
+ useCase = new SendEmailUseCase(
+ emailValidatorService,
+ emailDefaultConfigPort,
+ emailPort,
+ emailRepository
+ );
+
+ when(emailDefaultConfigPort.getDefaultRecipient()).thenReturn("default@example.com");
+ }
+
+ @Test
+ void shouldSendEmailSuccessfully() {
+ // given
+ EmailDTO dto = new EmailDTO("sender@example.com", "Subject", "Body");
+ when(emailPort.sendEmail(any(Email.class)))
+ .thenAnswer(invocation -> invocation.getArgument(0));
+
+ // when
+ useCase.handle(dto);
+
+ // then
+ verify(emailValidatorService).validate(any(Email.class));
+ verify(emailPort).sendEmail(any(Email.class));
+ verify(emailRepository).save(argThat(email ->
+ email.getStatus() == EmailStatus.SENT
+ ));
+ }
+
+ @Test
+ void shouldMarkEmailAsFailedWhenSendThrowsException() {
+ // given
+ EmailDTO dto = new EmailDTO("sender@example.com", "Subject", "Body");
+ when(emailPort.sendEmail(any(Email.class))).thenThrow(new RuntimeException("SMTP error"));
+
+ // when
+ useCase.handle(dto);
+
+ // then
+ verify(emailValidatorService).validate(any(Email.class));
+ verify(emailRepository).save(argThat(email ->
+ email.getStatus() == EmailStatus.FAILED &&
+ email.getErrorDescription().contains("SMTP error")
+ ));
+ }
+
+ @Test
+ void shouldUseDefaultRecipientFromConfigPort() {
+ // given
+ EmailDTO dto = new EmailDTO("sender@example.com", "Subject", "Body");
+
+ when(emailPort.sendEmail(any(Email.class)))
+ .thenAnswer(invocation -> invocation.getArgument(0));
+
+ // when
+ useCase.handle(dto);
+
+ // then
+ verify(emailRepository).save(argThat(email ->
+ email.getTo().equals("default@example.com")
+ ));
+ }
+
+ @Test
+ void shouldPropagateExceptionWhenValidatorFails() {
+ // given
+ EmailDTO dto = new EmailDTO("sender@example.com", "Subject", "Body");
+
+ doThrow(new RuntimeException("Invalid email")).when(emailValidatorService).validate(any());
+
+ // when & then
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> useCase.handle(dto));
+ assertThat(ex.getMessage()).isEqualTo("Invalid email");
+
+ // El repositorio no debería guardar el email, porque la validación falló antes
+ verify(emailRepository, never()).save(any());
+ }
+
+ @Test
+ void shouldFailWhenEmailDTOHasNullFields() {
+ // given
+ EmailDTO dto = new EmailDTO(null, null, null);
+
+ doThrow(new IllegalArgumentException("Invalid fields")).when(emailValidatorService).validate(any());
+
+ // when & then
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> useCase.handle(dto));
+ assertThat(ex.getMessage()).isEqualTo("Invalid fields");
+
+ verify(emailRepository, never()).save(any());
+ }
+}
\ No newline at end of file
diff --git a/domain/pom.xml b/domain/pom.xml
index 5e99338..c4443bb 100644
--- a/domain/pom.xml
+++ b/domain/pom.xml
@@ -21,5 +21,33 @@
slf4j-api
2.0.13
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 5.14.2
+ test
+
+
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
+
+
+ maven_central
+ Maven Central
+ https://repo.maven.apache.org/maven2/
+
+
\ No newline at end of file
diff --git a/domain/src/test/java/com/pablotj/restemailbridge/domain/service/EmailValidatorServiceTest.java b/domain/src/test/java/com/pablotj/restemailbridge/domain/service/EmailValidatorServiceTest.java
new file mode 100644
index 0000000..65a49b6
--- /dev/null
+++ b/domain/src/test/java/com/pablotj/restemailbridge/domain/service/EmailValidatorServiceTest.java
@@ -0,0 +1,62 @@
+package com.pablotj.restemailbridge.domain.service;
+
+import com.pablotj.restemailbridge.domain.model.Email;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class EmailValidatorServiceTest {
+
+ private final EmailValidatorService validator = new EmailValidatorService();
+
+ @Test
+ void shouldThrowIfEmailIsNull() {
+ assertThatThrownBy(() -> validator.validate(null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Email cannot be null");
+ }
+
+ @Test
+ void shouldThrowIfRecipientInvalid() {
+ Email email = Email.create("sender@example.com", "not-an-email", "Subject", "Body");
+
+ assertThatThrownBy(() -> validator.validate(email))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Recipient email is invalid");
+ }
+
+ @Test
+ void shouldThrowIfSenderIsBlank() {
+ Email email = Email.create("", "recipient@example.com", "Subject", "Body");
+
+ assertThatThrownBy(() -> validator.validate(email))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Sender email is required");
+ }
+
+ @Test
+ void shouldThrowIfSubjectIsBlank() {
+ Email email = Email.create("sender@example.com", "recipient@example.com", "", "Body");
+
+ assertThatThrownBy(() -> validator.validate(email))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Subject is required");
+ }
+
+ @Test
+ void shouldThrowIfBodyIsBlank() {
+ Email email = Email.create("sender@example.com", "recipient@example.com", "Subject", "");
+
+ assertThatThrownBy(() -> validator.validate(email))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Body is required");
+ }
+
+ @Test
+ void shouldPassForValidEmail() {
+ Email email = Email.create("sender@example.com", "recipient@example.com", "Subject", "Body");
+
+ // no debe lanzar excepción
+ validator.validate(email);
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/pom.xml b/infrastructure/pom.xml
index 9e4fd99..adef519 100644
--- a/infrastructure/pom.xml
+++ b/infrastructure/pom.xml
@@ -149,6 +149,24 @@
lombok
true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.h2database
+ h2
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
diff --git a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionConverter.java b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/encryption/EncryptionConverter.java
similarity index 90%
rename from infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionConverter.java
rename to infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/encryption/EncryptionConverter.java
index 5897474..f73fef3 100644
--- a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionConverter.java
+++ b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/encryption/EncryptionConverter.java
@@ -1,4 +1,4 @@
-package com.pablotj.restemailbridge.infrastructure.persistence;
+package com.pablotj.restemailbridge.infrastructure.encryption;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
diff --git a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionUtils.java b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/encryption/EncryptionUtils.java
similarity index 97%
rename from infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionUtils.java
rename to infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/encryption/EncryptionUtils.java
index eeec59c..67a7ede 100644
--- a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionUtils.java
+++ b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/encryption/EncryptionUtils.java
@@ -1,13 +1,13 @@
-package com.pablotj.restemailbridge.infrastructure.persistence;
+package com.pablotj.restemailbridge.infrastructure.encryption;
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
public class EncryptionUtils {
diff --git a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/MailJpa.java b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/MailJpa.java
index 5ad3a0f..9a8fb74 100644
--- a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/MailJpa.java
+++ b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/MailJpa.java
@@ -1,6 +1,7 @@
package com.pablotj.restemailbridge.infrastructure.persistence;
import com.pablotj.restemailbridge.domain.model.EmailStatus;
+import com.pablotj.restemailbridge.infrastructure.encryption.EncryptionConverter;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
diff --git a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/rest/GlobalExceptionHandler.java b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/rest/GlobalExceptionHandler.java
index 2c87116..f97120b 100644
--- a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/rest/GlobalExceptionHandler.java
+++ b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/rest/GlobalExceptionHandler.java
@@ -17,4 +17,10 @@ public class GlobalExceptionHandler {
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
+
+ @ExceptionHandler(RuntimeException.class)
+ public ResponseEntity