diff --git a/pom.xml b/pom.xml
index 0858259..6d5bc94 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,10 @@
org.springframework.boot
spring-boot-starter-mail
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
org.projectlombok
diff --git a/src/integration/java/de/nclazz/service/mailrelay/AccountCrudTest.java b/src/integration/java/de/nclazz/service/mailrelay/AccountCrudTest.java
index e84a0ba..f2b2c1c 100644
--- a/src/integration/java/de/nclazz/service/mailrelay/AccountCrudTest.java
+++ b/src/integration/java/de/nclazz/service/mailrelay/AccountCrudTest.java
@@ -87,4 +87,19 @@ public class AccountCrudTest {
.andExpect(jsonPath("$.receivers", is(List.of("vip@account.com"))));
}
+ @Test
+ void createAccountReturns400OnInvalidName() throws Exception {
+ AccountForm form = new AccountForm("-- totally invalid --", List.of("vip@my-company.com"));
+ mockMvc.perform(post("/accounts").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(form)))
+ .andExpect(status().is4xxClientError())
+ .andExpect(content().contentType(APPLICATION_JSON));
+ }
+
+ @Test
+ void createAccountReturns400OnInvalidReceiver() throws Exception {
+ AccountForm form = new AccountForm("valid-name", List.of("valid@my-company.com", "in valid @my-company.com"));
+ mockMvc.perform(post("/accounts").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(form)))
+ .andExpect(status().is4xxClientError())
+ .andExpect(content().contentType(APPLICATION_JSON));
+ }
}
diff --git a/src/integration/java/de/nclazz/service/mailrelay/IntegrationTestConfiguration.java b/src/integration/java/de/nclazz/service/mailrelay/IntegrationTestConfiguration.java
index 230a6e9..3ca5911 100644
--- a/src/integration/java/de/nclazz/service/mailrelay/IntegrationTestConfiguration.java
+++ b/src/integration/java/de/nclazz/service/mailrelay/IntegrationTestConfiguration.java
@@ -1,7 +1,7 @@
package de.nclazz.service.mailrelay;
+import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import javax.mail.Session;
@@ -10,7 +10,7 @@ import javax.mail.internet.MimeMessage;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-@Configuration
+@TestConfiguration
public class IntegrationTestConfiguration {
@Bean
diff --git a/src/integration/java/de/nclazz/service/mailrelay/MailMessageForwarderTest.java b/src/integration/java/de/nclazz/service/mailrelay/MailMessageForwarderTest.java
index 66367b2..e623c81 100644
--- a/src/integration/java/de/nclazz/service/mailrelay/MailMessageForwarderTest.java
+++ b/src/integration/java/de/nclazz/service/mailrelay/MailMessageForwarderTest.java
@@ -7,7 +7,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.test.context.ActiveProfiles;
@@ -17,19 +18,28 @@ import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@SpringBootTest
-@ActiveProfiles("integration")
+@ActiveProfiles({ "integration", "non-async" })
public class MailMessageForwarderTest {
+ static JavaMailSender javaMailSender = mock(JavaMailSender.class);
+ static MimeMessage mimeMessage = new MimeMessage((Session) null);
+
+ @TestConfiguration
+ static class MailTestConfiguration {
+ @Bean
+ public JavaMailSender javaMailSender() {
+ when(javaMailSender.createMimeMessage()).thenReturn(mimeMessage);
+ return javaMailSender;
+ }
+ }
+
@Autowired
private Relay relay;
- @MockBean
- private JavaMailSender javaMailSender;
-
- private MimeMessage mimeMessage = new MimeMessage((Session) null);
@BeforeEach
void setup() {
diff --git a/src/integration/java/de/nclazz/service/mailrelay/MailRelayApplicationTest.java b/src/integration/java/de/nclazz/service/mailrelay/MailRelayApplicationTest.java
index 2993911..18aa55b 100644
--- a/src/integration/java/de/nclazz/service/mailrelay/MailRelayApplicationTest.java
+++ b/src/integration/java/de/nclazz/service/mailrelay/MailRelayApplicationTest.java
@@ -2,8 +2,10 @@ package de.nclazz.service.mailrelay;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
+@ActiveProfiles("integration")
class MailRelayApplicationTest {
@Test
diff --git a/src/main/java/de/nclazz/service/mailrelay/AsyncConfiguration.java b/src/main/java/de/nclazz/service/mailrelay/AsyncConfiguration.java
new file mode 100644
index 0000000..22512fe
--- /dev/null
+++ b/src/main/java/de/nclazz/service/mailrelay/AsyncConfiguration.java
@@ -0,0 +1,11 @@
+package de.nclazz.service.mailrelay;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@Configuration
+@EnableAsync
+@Profile("!non-async")
+public class AsyncConfiguration {
+}
diff --git a/src/main/java/de/nclazz/service/mailrelay/MailRelayApplication.java b/src/main/java/de/nclazz/service/mailrelay/MailRelayApplication.java
index ede1b40..0f062c4 100644
--- a/src/main/java/de/nclazz/service/mailrelay/MailRelayApplication.java
+++ b/src/main/java/de/nclazz/service/mailrelay/MailRelayApplication.java
@@ -3,11 +3,9 @@ package de.nclazz.service.mailrelay;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
-import org.springframework.scheduling.annotation.EnableAsync;
import java.time.Clock;
-@EnableAsync
@SpringBootApplication
public class MailRelayApplication {
diff --git a/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountForm.java b/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountForm.java
index b448024..573d233 100644
--- a/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountForm.java
+++ b/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountForm.java
@@ -1,10 +1,14 @@
package de.nclazz.service.mailrelay.adapter.web;
import de.nclazz.service.mailrelay.domain.Account;
+import de.nclazz.service.mailrelay.domain.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
import java.util.List;
@Data
@@ -12,8 +16,13 @@ import java.util.List;
@NoArgsConstructor
public class AccountForm {
+ @NotNull
+ @NotBlank
+ @Pattern(regexp = "^[^-_](?:[a-zA-Z_-]*[^-_])?$")
private String name;
- private List receivers;
+
+ @NotNull
+ private List<@Pattern(regexp = StringUtils.EMAIL_RFC_5322_REGEX) String> receivers;
public Account toAccount() {
return Account.of(this.name, this.receivers);
diff --git a/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountRestController.java b/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountRestController.java
index 166eacf..f226a3d 100644
--- a/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountRestController.java
+++ b/src/main/java/de/nclazz/service/mailrelay/adapter/web/AccountRestController.java
@@ -5,18 +5,23 @@ import de.nclazz.service.mailrelay.domain.Relay;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
+import javax.validation.Valid;
import java.util.List;
import java.util.UUID;
+import java.util.stream.Collectors;
@RestController
@RequestMapping("accounts")
@@ -26,7 +31,7 @@ public class AccountRestController {
private final Relay relay;
@PostMapping
- public ResponseEntity addAccount(@RequestBody AccountForm form) {
+ public ResponseEntity addAccount(@RequestBody @Valid AccountForm form) {
Account account = form.toAccount();
account = this.relay.saveAccount(account);
return ResponseEntity.status(HttpStatus.CREATED).body(account);
@@ -50,11 +55,20 @@ public class AccountRestController {
}
@PutMapping("{guid}")
- public Account updateByGuid(@PathVariable("guid")UUID guid, @RequestBody AccountForm form) {
+ public Account updateByGuid(@PathVariable("guid")UUID guid, @RequestBody @Valid AccountForm form) {
Account account = this.relay.findAccount(guid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
account.setName(form.getName());
account.setReceivers(form.getReceivers());
return this.relay.saveAccount(account);
}
+
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public List handleValidationExceptions(MethodArgumentNotValidException ex) {
+ return ex.getBindingResult()
+ .getAllErrors().stream()
+ .map(ValidationError::fromObjectError)
+ .collect(Collectors.toList());
+ }
}
diff --git a/src/main/java/de/nclazz/service/mailrelay/adapter/web/ValidationError.java b/src/main/java/de/nclazz/service/mailrelay/adapter/web/ValidationError.java
new file mode 100644
index 0000000..78d5cf5
--- /dev/null
+++ b/src/main/java/de/nclazz/service/mailrelay/adapter/web/ValidationError.java
@@ -0,0 +1,26 @@
+package de.nclazz.service.mailrelay.adapter.web;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ValidationError {
+
+ private String target;
+ private String message;
+ private Object rejected;
+
+ public static ValidationError fromObjectError(ObjectError error) {
+ return new ValidationError(
+ ((FieldError)error).getField(),
+ error.getDefaultMessage(),
+ ((FieldError) error).getRejectedValue()
+ );
+ }
+
+}
diff --git a/src/main/java/de/nclazz/service/mailrelay/domain/Message.java b/src/main/java/de/nclazz/service/mailrelay/domain/Message.java
index aae16f2..2309450 100644
--- a/src/main/java/de/nclazz/service/mailrelay/domain/Message.java
+++ b/src/main/java/de/nclazz/service/mailrelay/domain/Message.java
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
@@ -54,6 +55,7 @@ public class Message {
@Column(name = "created_at")
private LocalDateTime timestamp;
+ @ToString.Exclude
@JsonIgnore
@OneToOne
private Account account;
diff --git a/src/main/java/de/nclazz/service/mailrelay/domain/StringUtils.java b/src/main/java/de/nclazz/service/mailrelay/domain/StringUtils.java
index de69a2c..027bc0d 100644
--- a/src/main/java/de/nclazz/service/mailrelay/domain/StringUtils.java
+++ b/src/main/java/de/nclazz/service/mailrelay/domain/StringUtils.java
@@ -2,6 +2,8 @@ package de.nclazz.service.mailrelay.domain;
public abstract class StringUtils {
+ public static final String EMAIL_RFC_5322_REGEX = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])";
+
private StringUtils() { /* no-op */ }
public static boolean isStringEmpty(String input) {
@@ -11,4 +13,6 @@ public abstract class StringUtils {
return input.trim().isEmpty();
}
+
+
}
diff --git a/src/test/java/de/nclazz/service/mailrelay/adapter/web/AccountFormValidationTest.java b/src/test/java/de/nclazz/service/mailrelay/adapter/web/AccountFormValidationTest.java
new file mode 100644
index 0000000..2e06b88
--- /dev/null
+++ b/src/test/java/de/nclazz/service/mailrelay/adapter/web/AccountFormValidationTest.java
@@ -0,0 +1,87 @@
+package de.nclazz.service.mailrelay.adapter.web;
+
+import org.hibernate.validator.HibernateValidator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AccountFormValidationTest {
+
+ private LocalValidatorFactoryBean localValidatorFactory;
+
+ @BeforeEach
+ public void setup() {
+ localValidatorFactory = new LocalValidatorFactoryBean();
+ localValidatorFactory.setProviderClass(HibernateValidator.class);
+ localValidatorFactory.afterPropertiesSet();
+ }
+
+ @Test
+ void nameAsEmptyStringShouldBeNotValid() {
+ AccountForm form = new AccountForm("", List.of());
+
+ assertThat(localValidatorFactory.validate(form))
+ .isNotEmpty();
+ }
+
+ @Test
+ void nameIsMissingShouldBeNotValid() {
+ AccountForm form = new AccountForm(null, List.of());
+
+ assertThat(localValidatorFactory.validate(form))
+ .isNotEmpty();
+ }
+
+ @Test
+ void nameWithSingleCharacterShouldBeValid() {
+ AccountForm form = new AccountForm("a", List.of());
+
+ assertThat(localValidatorFactory.validate(form))
+ .hasSize(0);
+ }
+
+ @Test
+ void nameContainingNonAlphaNumericCharsShouldBeNotValid() {
+ AccountForm form = new AccountForm("a_.+a", List.of());
+
+ assertThat(localValidatorFactory.validate(form))
+ .hasSize(1);
+ }
+
+ @Test
+ void nameStartingWithHyphenShouldBeNotValid() {
+ AccountForm form = new AccountForm("-name", List.of());
+
+ assertThat(localValidatorFactory.validate(form))
+ .hasSize(1);
+ }
+
+ @Test
+ void nameContainingWithHyphenShouldBeValid() {
+ AccountForm form = new AccountForm("prefix-name", List.of());
+
+ assertThat(localValidatorFactory.validate(form))
+ .hasSize(0);
+ }
+
+ @Test
+ void receiversWithEmailAddressesShouldBeValid() {
+ AccountForm form = new AccountForm("legal-name", List.of("vip@company.com", "mail@example.com"));
+
+ assertThat(localValidatorFactory.validate(form))
+ .hasSize(0);
+ }
+
+ @Test
+ void receiversWithInvalidEmailAddressesShouldBeVNotalid() {
+ AccountForm form = new AccountForm("legal-name", List.of("vip @ company.com", "--mail@example.com"));
+
+ assertThat(localValidatorFactory.validate(form))
+ .hasSize(1);
+ }
+
+}
diff --git a/src/test/java/de/nclazz/service/mailrelay/domain/ForwardMessageTest.java b/src/test/java/de/nclazz/service/mailrelay/domain/ForwardMessageTest.java
index 1551ff5..44e8fa6 100644
--- a/src/test/java/de/nclazz/service/mailrelay/domain/ForwardMessageTest.java
+++ b/src/test/java/de/nclazz/service/mailrelay/domain/ForwardMessageTest.java
@@ -76,7 +76,9 @@ class ForwardMessageTest {
List.of("info@company.com")
);
- Relay relay = TestRelayBuilder.builder().build();
+ Relay relay = TestRelayBuilder.builder()
+ .messageForwarder(forwarder)
+ .build();
relay.saveAccount(account);
LocalDateTime now = LocalDateTime.now();