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();