Compare commits
7 Commits
f04fb14192
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a42a8dd3a5 | |||
| ab0e2ffa68 | |||
| e0692f7913 | |||
| bcb97312cd | |||
| 8fa2845220 | |||
| fea733f990 | |||
| ee88068d99 |
@@ -1,6 +1,7 @@
|
|||||||
SPRING_PROFILES_ACTIVE=dev
|
SPRING_PROFILES_ACTIVE=dev
|
||||||
|
|
||||||
APP_ENCRYPTION_SECRET=123456789
|
APP_ENCRYPTION_SECRET=123456789
|
||||||
|
APP_ALLOWED_ORIGINS=http://localhost:8080
|
||||||
|
|
||||||
DB_NAME=EXAMPLE_DB
|
DB_NAME=EXAMPLE_DB
|
||||||
DB_USER=EXAMPLE
|
DB_USER=EXAMPLE
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ COPY --from=build /app/bootstrap/target/*.jar app.jar
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
EXPOSE 5005
|
EXPOSE 5005
|
||||||
|
|
||||||
ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "app.jar"]
|
ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-Xmx512m", "-Xms256m", "-jar", "app.jar"]
|
||||||
@@ -23,5 +23,25 @@
|
|||||||
<version>2.0.13</version>
|
<version>2.0.13</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.10.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.14.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.24.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.pablotj.restemailbridge.bootstrap;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
public class EnvironmentValidatorInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(ConfigurableApplicationContext ctx) {
|
||||||
|
String[] requiredProperties = {
|
||||||
|
"APP_ENCRYPTION_SECRET",
|
||||||
|
"APP_ALLOWED_ORIGINS",
|
||||||
|
"DB_NAME",
|
||||||
|
"DB_USER",
|
||||||
|
"DB_PASSWORD",
|
||||||
|
"DB_HOST",
|
||||||
|
"DB_PORT",
|
||||||
|
"GMAIL_OAUTH_CLIENT_ID",
|
||||||
|
"GMAIL_OAUTH_CLIENT_SECRET"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String prop : requiredProperties) {
|
||||||
|
String value = ctx.getEnvironment().getProperty(prop);
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
throw new IllegalStateException("ERROR: Property '" + prop + "' is not defined or empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
|||||||
public class RestEmailBridgeApplication {
|
public class RestEmailBridgeApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(RestEmailBridgeApplication.class, args);
|
SpringApplication app = new SpringApplication(RestEmailBridgeApplication.class);
|
||||||
|
app.addInitializers(new EnvironmentValidatorInitializer());
|
||||||
|
app.run(args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
info:
|
info:
|
||||||
app:
|
app:
|
||||||
version: @project.version@
|
version: @project.version@
|
||||||
|
|
||||||
app:
|
app:
|
||||||
encryption:
|
encryption:
|
||||||
secret: ${APP_ENCRYPTION_SECRET}
|
secret: ${APP_ENCRYPTION_SECRET}
|
||||||
|
cors:
|
||||||
|
allowed-origins: ${APP_ALLOWED_ORIGINS:http://localhost:8080}
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
servlet:
|
||||||
|
context-path: /api
|
||||||
|
forward-headers-strategy: framework
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: restemailbridge
|
name: restemailbridge
|
||||||
@@ -11,6 +21,18 @@ spring:
|
|||||||
resources:
|
resources:
|
||||||
add-mappings: false
|
add-mappings: false
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:default_db}
|
||||||
|
username: ${DB_USER:postgres}
|
||||||
|
password: ${DB_PASSWORD:postgres}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 3
|
||||||
|
minimum-idle: 1
|
||||||
|
idle-timeout: 30000
|
||||||
|
connection-timeout: 10000
|
||||||
|
leak-detection-threshold: 10000
|
||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: validate
|
||||||
@@ -18,6 +40,7 @@ spring:
|
|||||||
hibernate.transaction.jta.platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform
|
hibernate.transaction.jta.platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform
|
||||||
hibernate:
|
hibernate:
|
||||||
format_sql: true
|
format_sql: true
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
show-sql: true
|
show-sql: true
|
||||||
|
|
||||||
jackson:
|
jackson:
|
||||||
@@ -31,31 +54,8 @@ springdoc:
|
|||||||
path: /swagger-ui
|
path: /swagger-ui
|
||||||
show-actuator: true
|
show-actuator: true
|
||||||
|
|
||||||
server:
|
|
||||||
port: 8080
|
|
||||||
servlet:
|
|
||||||
context-path: /api
|
|
||||||
forward-headers-strategy: framework
|
|
||||||
|
|
||||||
gmail:
|
gmail:
|
||||||
oauth2:
|
oauth2:
|
||||||
clientId: ${GMAIL_OAUTH_CLIENT_ID}
|
clientId: ${GMAIL_OAUTH_CLIENT_ID}
|
||||||
clientSecret: ${GMAIL_OAUTH_CLIENT_SECRET}
|
clientSecret: ${GMAIL_OAUTH_CLIENT_SECRET}
|
||||||
redirectUri: http://localhost:8888/Callback
|
redirectUri: http://localhost:8888/Callback
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
spring:
|
|
||||||
config:
|
|
||||||
activate:
|
|
||||||
on-profile: default
|
|
||||||
|
|
||||||
datasource:
|
|
||||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:default_db}
|
|
||||||
username: ${DB_USER:postgres}
|
|
||||||
password: ${DB_PASSWORD:postgres}
|
|
||||||
driver-class-name: org.postgresql.Driver
|
|
||||||
jpa:
|
|
||||||
properties:
|
|
||||||
hibernate:
|
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
@@ -14,6 +14,7 @@ services:
|
|||||||
DB_USER: ${DB_USER}
|
DB_USER: ${DB_USER}
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
APP_ENCRYPTION_SECRET: ${APP_ENCRYPTION_SECRET}
|
APP_ENCRYPTION_SECRET: ${APP_ENCRYPTION_SECRET}
|
||||||
|
APP_ALLOWED_ORIGINS: ${APP_ALLOWED_ORIGINS}
|
||||||
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
|
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
|
||||||
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
|
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -21,5 +21,33 @@
|
|||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
<version>2.0.13</version>
|
<version>2.0.13</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.10.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.14.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.24.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>maven_central</id>
|
||||||
|
<name>Maven Central</name>
|
||||||
|
<url>https://repo.maven.apache.org/maven2/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
</project>
|
</project>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,11 @@
|
|||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.reflections</groupId>
|
<groupId>org.reflections</groupId>
|
||||||
<artifactId>reflections</artifactId>
|
<artifactId>reflections</artifactId>
|
||||||
@@ -149,6 +154,24 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.config;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class CorsConfig {
|
||||||
|
|
||||||
|
@Value("${app.cors.allowed-origins}")
|
||||||
|
private String allowedOriginsString;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
|
||||||
|
List<String> allowedOrigins = Arrays.asList(allowedOriginsString.split(","));
|
||||||
|
config.setAllowedOriginPatterns(allowedOrigins);
|
||||||
|
|
||||||
|
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
config.setAllowedHeaders(List.of("*"));
|
||||||
|
config.setAllowCredentials(true);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", config);
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final CorsConfigurationSource corsConfigurationSource;
|
||||||
|
|
||||||
|
public SecurityConfig(CorsConfigurationSource corsConfigurationSource) {
|
||||||
|
this.corsConfigurationSource = corsConfigurationSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.pablotj.restemailbridge.infrastructure.persistence;
|
package com.pablotj.restemailbridge.infrastructure.encryption;
|
||||||
|
|
||||||
import jakarta.persistence.AttributeConverter;
|
import jakarta.persistence.AttributeConverter;
|
||||||
import jakarta.persistence.Converter;
|
import jakarta.persistence.Converter;
|
||||||
@@ -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.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Base64;
|
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 {
|
public class EncryptionUtils {
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.pablotj.restemailbridge.infrastructure.persistence;
|
package com.pablotj.restemailbridge.infrastructure.persistence;
|
||||||
|
|
||||||
import com.pablotj.restemailbridge.domain.model.EmailStatus;
|
import com.pablotj.restemailbridge.domain.model.EmailStatus;
|
||||||
|
import com.pablotj.restemailbridge.infrastructure.encryption.EncryptionConverter;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Convert;
|
import jakarta.persistence.Convert;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
|||||||
@@ -17,4 +17,10 @@ public class GlobalExceptionHandler {
|
|||||||
errors.put(error.getField(), error.getDefaultMessage()));
|
errors.put(error.getField(), error.getDefaultMessage()));
|
||||||
return ResponseEntity.badRequest().body(errors);
|
return ResponseEntity.badRequest().body(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException ex) {
|
||||||
|
Map<String, String> error = Map.of("error", ex.getMessage());
|
||||||
|
return ResponseEntity.unprocessableEntity().body(error); // 422
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
|
||||||
|
@SpringBootApplication(scanBasePackages = "com.pablotj.restemailbridge")
|
||||||
|
@EnableJpaRepositories(basePackages = "com.pablotj.restemailbridge")
|
||||||
|
@EntityScan(basePackages = "com.pablotj.restemailbridge")
|
||||||
|
public class RestEmailBridgeTestApplication {
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.persistence;
|
||||||
|
|
||||||
|
import com.pablotj.restemailbridge.domain.model.Email;
|
||||||
|
import com.pablotj.restemailbridge.domain.model.EmailStatus;
|
||||||
|
import com.pablotj.restemailbridge.infrastructure.config.JpaConfig;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Import(JpaConfig.class)
|
||||||
|
@DataJpaTest
|
||||||
|
class MailRepositoryAdapterIT {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SpringDataMailRepository springDataMailRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSaveEmailSuccessfully() {
|
||||||
|
MailRepositoryAdapter adapter = new MailRepositoryAdapter(springDataMailRepository);
|
||||||
|
|
||||||
|
Email email = Email.create("sender@example.com", "recipient@example.com", "Subject", "Body");
|
||||||
|
|
||||||
|
Email saved = adapter.save(email);
|
||||||
|
|
||||||
|
assertThat(saved.getFrom()).isEqualTo("sender@example.com");
|
||||||
|
assertThat(saved.getTo()).isEqualTo("recipient@example.com");
|
||||||
|
assertThat(saved.getSubject()).isEqualTo("Subject");
|
||||||
|
assertThat(saved.getBody()).isEqualTo("Body");
|
||||||
|
assertThat(saved.getStatus()).isEqualTo(EmailStatus.PENDING);
|
||||||
|
|
||||||
|
assertThat(springDataMailRepository.findAll())
|
||||||
|
.hasSize(1)
|
||||||
|
.first()
|
||||||
|
.extracting("sender", "recipient")
|
||||||
|
.containsExactly("sender@example.com", "recipient@example.com");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.rest;
|
||||||
|
|
||||||
|
import com.pablotj.restemailbridge.application.port.out.EmailPort;
|
||||||
|
import com.pablotj.restemailbridge.infrastructure.RestEmailBridgeTestApplication;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@SpringBootTest(
|
||||||
|
classes = RestEmailBridgeTestApplication.class,
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
|
||||||
|
)
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class MailControllerIT {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private EmailPort emailPort;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturn200WhenEmailIsSent() throws Exception {
|
||||||
|
when(emailPort.sendEmail(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/v1/mail")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"from\":\"sender@example.com\",\"subject\":\"Subject\",\"body\":\"Body\"}"))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
|
verify(emailPort).sendEmail(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturn400WhenRequestIsInvalid() throws Exception {
|
||||||
|
mockMvc.perform(post("/v1/mail")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"subject\":\"Subject\",\"body\":\"Body\"}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
}
|
||||||
27
infrastructure/src/test/resources/application.yml
Normal file
27
infrastructure/src/test/resources/application.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: restemailbridge
|
||||||
|
datasource:
|
||||||
|
url: jdbc:h2:mem:restemailbridge;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||||
|
driver-class-name: org.h2.Driver
|
||||||
|
username: sa
|
||||||
|
password:
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: create-drop
|
||||||
|
show-sql: true
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 0
|
||||||
|
servlet:
|
||||||
|
context-path: /api
|
||||||
|
|
||||||
|
app:
|
||||||
|
encryption:
|
||||||
|
secret: 0123456789ABCDEF0123456789ABCDEF
|
||||||
|
|
||||||
|
gmail:
|
||||||
|
oauth2:
|
||||||
|
clientId: dummy
|
||||||
|
clientSecret: dummy
|
||||||
|
redirectUri: http://localhost:8888/Callback
|
||||||
Reference in New Issue
Block a user