mn create-app example.micronaut.micronautguide --build=maven --lang=java
Send Emails from Micronaut
Learn how to send emails with AWS SES and SendGrid from a Micronaut app and leverage @Requires annotation to load beans conditionally.
Authors: Sergio del Amo
Micronaut Version: 2.5.0
1. Getting Started
In this guide we are going to create a Micronaut app written in Java.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
3. Solution
We recommend that you follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Writing the App
Create an app using the Micronaut Command Line Interface or with Micronaut Launch.
If you don’t specify the --build argument, Gradle is used as a build tool. If you don’t specify the --lang argument, Java is used as a language.
|
The previous command creates a Micronaut app with the default package example.micronaut
in a folder named micronautguide
.
If you are using Java or Kotlin and IntelliJ IDEA, make sure you have enabled annotation processing.
4.1. Controller
Create MailController
which use a collaborator, emailService
to send and email.
package example.micronaut;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.Valid;
@Controller("/mail") (1)
public class MailController {
private final EmailService emailService;
public MailController(EmailService emailService) { (2)
this.emailService = emailService;
}
@Post("/send") (3)
public HttpResponse<?> send(@Body @Valid EmailCmd cmd) { (4)
emailService.send(cmd);
return HttpResponse.ok(); (5)
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /mail/send |
2 | Constructor injection |
3 | The @Post annotation is used to map the index method to all requests that use an HTTP POST |
4 | Add @Valid to any method parameter which requires validation. Use a POGO supplied as a JSON payload in the request to populate the email. |
5 | Return 200 OK as the result |
The previous controller uses a POJO supplied in the request body as a JSON Payload
@Introspected
public class EmailCmd implements Email {
@NotNull
@NotBlank
private String recipient;
@NotNull
@NotBlank
private String subject;
private List<String> cc = new ArrayList<>();
private List<String> bcc = new ArrayList<>();
private String htmlBody;
private String textBody;
private String replyTo;
@Override
public String getRecipient() {
return recipient;
}
public void setRecipient(String recipient) {
this.recipient = recipient;
}
@Override
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
@Override
public List<String> getCc() {
return cc;
}
public void setCc(List<String> cc) {
this.cc = cc;
}
@Override
public List<String> getBcc() {
return bcc;
}
public void setBcc(List<String> bcc) {
this.bcc = bcc;
}
@Override
public String getHtmlBody() {
return htmlBody;
}
public void setHtmlBody(String htmlBody) {
this.htmlBody = htmlBody;
}
@Override
public String getTextBody() {
return textBody;
}
public void setTextBody(String textBody) {
this.textBody = textBody;
}
@Override
public String getReplyTo() {
return replyTo;
}
public void setReplyTo(String replyTo) {
this.replyTo = replyTo;
}
}
4.2. Email Service
Create an interface - EmailService
. Any email provider present in the application should implement it.
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public interface EmailService {
void send(@NonNull @NotNull @Valid Email email);
}
package example.micronaut;
import java.util.List;
public interface Email {
String getRecipient();
List<String> getCc();
List<String> getBcc();
String getSubject();
String getHtmlBody();
String getTextBody();
String getReplyTo();
}
4.2.1. AWS SES
Amazon Simple Email Service (Amazon SES) is a cloud-based email sending service designed to help digital marketers and application developers send marketing, notification, and transactional emails. It is a reliable, cost-effective service for businesses of all sizes that use email to keep in contact with their customers.
Add a dependency to AWS SES SDK:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>ses:2.16.18</artifactId>
<scope>compile</scope>
</dependency>
Create service which uses AWS Simple Email Service client to send the email
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Secondary;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.ses.SesClient;
import software.amazon.awssdk.services.ses.model.Body;
import software.amazon.awssdk.services.ses.model.Content;
import software.amazon.awssdk.services.ses.model.Destination;
import software.amazon.awssdk.services.ses.model.Message;
import software.amazon.awssdk.services.ses.model.SendEmailRequest;
import software.amazon.awssdk.services.ses.model.SendEmailResponse;
import javax.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@Singleton (1)
@Requires(condition = AwsResourceAccessCondition.class) (2)
@Secondary (3)
public class AwsSesMailService implements EmailService {
private static final Logger LOG = LoggerFactory.getLogger(AwsSesMailService.class);
protected final String sourceEmail;
protected final SesClient ses;
public AwsSesMailService(@Nullable @Value("${AWS_REGION}") String awsRegionEnv, (4)
@Nullable @Value("${AWS_SOURCE_EMAIL}") String sourceEmailEnv,
@Nullable @Value("${aws.region}") String awsRegionProp,
@Nullable @Value("${aws.sourceemail}") String sourceEmailProp) {
this.sourceEmail = sourceEmailEnv != null ? sourceEmailEnv : sourceEmailProp;
String awsRegion = awsRegionEnv != null ? awsRegionEnv : awsRegionProp;
this.ses = SesClient.builder().region(Region.of(awsRegion)).build();
}
@Override
public void send(@NonNull @NotNull @Valid Email email) {
SendEmailRequest sendEmailRequest = SendEmailRequest.builder()
.destination(Destination.builder().toAddresses(email.getRecipient()).build())
.source(sourceEmail)
.message(Message.builder().subject(Content.builder().data(email.getSubject()).build())
.body(Body.builder().text(Content.builder().data(email.getTextBody()).build()).build()).build())
.build();
SendEmailResponse response =ses.sendEmail(sendEmailRequest);
if (LOG.isInfoEnabled()) {
LOG.info("Sent email with id: {}", response.messageId());
}
}
}
1 | Use javax.inject.Singleton to designate a class a a singleton |
2 | Bean will not loaded unless condition is met. |
3 | In case of multiple possible interface implementations of EmailService , @Secondary reduces the priority. |
4 | Values for region and source email are resolved from environment variables or system properties. |
We annotated the previous class with @Requires(condition = AwsResourceAccessCondition.class)
.
The AwsResourceAccessCondition
ensures the bean is not loaded unless certain conditions are fulfilled.
If your application creates an AWS client using the create method, the client searches for credentials using the default credentials provider chain, in the following order:
In the Java system properties:
aws.accessKeyId
andaws.secretAccessKey
.In system environment variables:
AWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
.In the default credentials file (the location of this file varies by platform).
n the Amazon ECS environment variable:
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
.In the instance profile credentials, which exist within the instance metadata associated with the IAM role for the EC2 instance.
package example.micronaut;
import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;
import io.micronaut.context.env.Environment;
import io.micronaut.core.util.StringUtils;
/**
* @see <a href="https://docs.aws.amazon.com/sdk-for-java/v2/developer-guide/java-dg-roles.html">Configure IAM Roles for Amazon EC2</a>
*/
public class AwsResourceAccessCondition implements Condition {
@Override
public boolean matches(ConditionContext context) {
if (StringUtils.isNotEmpty(System.getProperty("aws.accessKeyId")) && StringUtils.isNotEmpty(System.getProperty("aws.secretAccessKey"))) { (1)
return true;
}
if (StringUtils.isNotEmpty(System.getenv("AWS_ACCESS_KEY_ID")) && StringUtils.isNotEmpty(System.getenv("AWS_SECRET_ACCESS_KEY"))) { (2)
return true;
}
if (StringUtils.isNotEmpty(System.getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"))) { (3)
return true;
}
return context != null && context.getBean(Environment.class).getActiveNames().contains(Environment.AMAZON_EC2); (4)
}
}
Add a test to verify the service is loaded:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class AwsSesMailServiceTest {
@Test
public void awsSesMailServiceIsNotLoadedIfSystemPropertyIsNotPresent() {
ApplicationContext ctx = ApplicationContext.run();
assertFalse(ctx.containsBean(AwsSesMailService.class));
ctx.close();
}
@Test
public void awsSesMailServiceIsLoadedIfSystemPropertiesArePresent() {
String accesskeyid = System.getProperty("aws.accessKeyId");
String awssecretkey = System.getProperty("aws.secretAccessKey");
String awsregion = System.getProperty("aws.region");
String sourceemail = System.getProperty("aws.sourceemail");
System.setProperty("aws.accessKeyId", "XXXX");
System.setProperty("aws.secretAccessKey", "YKYY");
System.setProperty("aws.region", "XXXX");
System.setProperty("aws.sourceemail", "me@micronaut.example");
ApplicationContext ctx = ApplicationContext.run();
AwsSesMailService bean = ctx.getBean(AwsSesMailService.class);
assertEquals("me@micronaut.example", bean.sourceEmail);
ctx.close();
if (awsregion == null) {
System.clearProperty("aws.region");
} else {
System.setProperty("aws.region", awsregion);
}
if (sourceemail == null) {
System.clearProperty("aws.sourceemail");
} else {
System.setProperty("aws.sourceemail", sourceemail);
}
if (accesskeyid == null) {
System.clearProperty("aws.accessKeyId");
} else {
System.setProperty("aws.accessKeyId", accesskeyid);
}
if (awssecretkey == null) {
System.clearProperty("aws.secretAccessKey");
} else {
System.setProperty("aws.secretAccessKey", awssecretkey);
}
}
}
4.2.2. SendGrid
SendGrid is a transactional email service.
SendGrid is responsible for sending billions of emails for some of the best and brightest companies in the world.
Add a dependency to SendGrid SDK:
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java:4.7.2</artifactId>
<scope>compile</scope>
</dependency>
Create a service which encapsulates the integration with SendGrid. The bean will not be loaded if the
system properties (sendgrid.apikey
, sendgrid.fromemail
) or environment variables (SENDGRID_APIKEY
, SENDGRID_FROM_EMAIL
) are not present.
package example.micronaut;
import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;
import io.micronaut.core.util.StringUtils;
public class SendGridEmailCondition implements Condition {
@Override
public boolean matches(ConditionContext context) {
return envOrSystemProperty("SENDGRID_APIKEY", "sendgrid.apikey") &&
envOrSystemProperty("SENDGRID_FROM_EMAIL", "sendgrid.fromemail");
}
private boolean envOrSystemProperty(String env, String prop) {
return StringUtils.isNotEmpty(System.getProperty(prop)) || StringUtils.isNotEmpty(System.getenv(env));
}
}
Add a test:
package example.micronaut;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SendGridEmailConditionTest {
@Test
public void conditionIsTrueIfSystemPropertiesArePresent() {
String sendGridApiKey = System.getProperty("sendgrid.apikey");
String sendGrindFromEmail = System.getProperty("sendgrid.fromemail");
System.setProperty("sendgrid.apikey", "XXXX");
System.setProperty("sendgrid.fromemail", "me@micronaut.example");
SendGridEmailCondition condition = new SendGridEmailCondition();
assertTrue(condition.matches(null));
if (sendGridApiKey == null) {
System.clearProperty("sendgrid.apikey");
} else {
System.setProperty("sendgrid.apikey", sendGridApiKey);
}
if (sendGrindFromEmail == null) {
System.clearProperty("sendgrid.fromemail");
} else {
System.setProperty("sendgrid.fromemail", sendGrindFromEmail);
}
}
@Test
public void conditionIsFalseIfSystemPropertiesAreNotPresent() {
SendGridEmailCondition condition = new SendGridEmailCondition();
assertFalse(condition.matches(null));
}
}
package example.micronaut;
import com.sendgrid.SendGrid;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.Method;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Personalization;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.IOException;
@Singleton (1)
@Requires(condition = SendGridEmailCondition.class) (2)
class SendGridEmailService implements EmailService {
private static final Logger LOG = LoggerFactory.getLogger(SendGridEmailService.class);
protected final String apiKey;
protected final String fromEmail;
SendGridEmailService(@Value("${SENDGRID_APIKEY:none}") String apiKeyEnv, (3)
@Value("${SENDGRID_FROM_EMAIL:none}") String fromEmailEnv,
@Value("${sendgrid.apikey:none}") String apiKeyProp,
@Value("${sendgrid.fromemail:none}") String fromEmailProp) {
this.apiKey = apiKeyEnv != null && !apiKeyEnv.equals("none") ? apiKeyEnv : apiKeyProp;
this.fromEmail = fromEmailEnv != null && !fromEmailEnv.equals("none") ? fromEmailEnv: fromEmailProp;
}
protected Content contentOfEmail(Email email) {
if ( email.getTextBody() !=null ) {
return new Content("text/plain", email.getTextBody());
}
if ( email.getHtmlBody() !=null ) {
return new Content("text/html", email.getHtmlBody());
}
return null;
}
@Override
public void send(@NonNull @NotNull @Valid Email email) {
Personalization personalization = new Personalization();
personalization.setSubject(email.getSubject());
com.sendgrid.helpers.mail.objects.Email to = new com.sendgrid.helpers.mail.objects.Email(email.getRecipient());
personalization.addTo(to);
if ( email.getCc() != null ) {
for ( String cc : email.getCc() ) {
com.sendgrid.helpers.mail.objects.Email ccEmail = new com.sendgrid.helpers.mail.objects.Email();
ccEmail.setEmail(cc);
personalization.addCc(ccEmail);
}
}
if ( email.getBcc() != null ) {
for ( String bcc : email.getBcc() ) {
com.sendgrid.helpers.mail.objects.Email bccEmail = new com.sendgrid.helpers.mail.objects.Email();
bccEmail.setEmail(bcc);
personalization.addBcc(bccEmail);
}
}
Mail mail = new Mail();
com.sendgrid.helpers.mail.objects.Email from = new com.sendgrid.helpers.mail.objects.Email();
from.setEmail(fromEmail);
mail.from = from;
mail.addPersonalization(personalization);
Content content = contentOfEmail(email);
mail.addContent(content);
SendGrid sg = new SendGrid(apiKey);
Request request = new Request();
try {
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
Response response = sg.api(request);
if (LOG.isInfoEnabled()) {
LOG.info("Status Code: {}", String.valueOf(response.getStatusCode()));
LOG.info("Body: {}", response.getBody());
for ( String k : response.getHeaders().keySet()) {
String v = response.getHeaders().get(k);
LOG.info("Response Header {} => {}", k, v);
}
}
} catch (IOException ex) {
if (LOG.isErrorEnabled()) {
LOG.error(ex.getMessage());
}
}
}
}
1 | Use javax.inject.Singleton to designate a class a a singleton |
2 | Bean will not loaded unless condition is met. |
3 | Values will be resolved from system properties. |
Add a test:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class SendGridEmailServiceTest {
@Test
public void sendGridEmailServiceIsNotLoadedIfSystemPropertyIsNotPresent() {
ApplicationContext ctx = ApplicationContext.run();
assertFalse(ctx.containsBean(SendGridEmailService.class));
ctx.close();
}
@Test
public void sendGridEmailServiceIsLoadedIfSystemPropertiesArePresent() {
String sendGridApiKey = System.getProperty("sendgrid.apikey");
String sendGrindFromEmail = System.getProperty("sendgrid.fromemail");
System.setProperty("sendgrid.apikey", "XXXX");
System.setProperty("sendgrid.fromemail", "me@micronaut.example");
ApplicationContext ctx = ApplicationContext.run();
SendGridEmailService bean = ctx.getBean(SendGridEmailService.class);
assertEquals("XXXX", bean.apiKey);
assertEquals("me@micronaut.example", bean.fromEmail);
ctx.close();
if (sendGridApiKey == null) {
System.clearProperty("sendgrid.apikey");
} else {
System.setProperty("sendgrid.apikey", sendGridApiKey);
}
if (sendGrindFromEmail == null) {
System.clearProperty("sendgrid.fromemail");
} else {
System.setProperty("sendgrid.fromemail", sendGrindFromEmail);
}
}
}
4.3. Run the app
Add a logger to get more visibility:
<logger name="example.micronaut" level="TRACE"/>
To use SendGrid, define the required environment variables and run the app
$ export SENDGRID_FROM_EMAIL=email@email.com
$ export SENDGRID_APIKEY=XXXXXX
$ ./mvnw mn:run
To use AWS SES, define the required environment variables and run the app
$ export AWS_REGION=eu-west-1
$ export AWS_SOURCE_EMAIL=email@email.com
$ export AWS_ACCESS_KEY_ID=XXXXXXXX
$ export AWS_SECRET_KEY=XXXXXXXX
$ ./mvnw mn:run
If you supply both AWS SES and SendGrid system properties, the SendGrid EmailService
implementation will be used due to the @Secondary
annotation in AwsSesMailService
.
curl -X "POST" "http://localhost:8080/mail/send" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"subject": "Test Email",
"recipient": "recipient@email.com",
"textBody": "Foo"
}'
4.4. Test
In our acceptance test, beans SendGridEmailService
or AwsSesMailService
will not be loaded since system properties are not present.
Instead, we setup a Mock which we can verify interactions against.
package example.micronaut;
import io.micronaut.context.annotation.Primary;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.NonNull;
import javax.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
@Primary
@Requires(property = "spec.name", value = "mailcontroller")
@Singleton
public class MockEmailService implements EmailService {
public List<Email> emails = new ArrayList<>();
@Override
public void send(@NonNull @NotNull @Valid Email email) {
emails.add(email);
}
}
Create the next test:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Property;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
import java.util.Collection;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@MicronautTest (1)
@Property(name = "spec.name", value = "mailcontroller") (2)
class MailControllerTest {
@Inject
ApplicationContext applicationContext; (3)
@Inject
@Client("/")
RxHttpClient client; (4)
@Test
public void mailsendInteractsOnceEmailService() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Test");
cmd.setRecipient("delamos@grails.example");
cmd.setTextBody("Hola hola");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (5)
Collection<EmailService> emailServices = applicationContext.getBeansOfType(EmailService.class);
assertEquals(1, emailServices.size());
EmailService emailService = applicationContext.getBean(EmailService.class);
assertTrue(emailService instanceof MockEmailService);
int oldEmailsSize = ((MockEmailService) emailService).emails.size();
HttpResponse<?> rsp = client.toBlocking().exchange(request);
assertEquals(HttpStatus.OK, rsp.getStatus());
assertEquals(oldEmailsSize + 1 , ((MockEmailService) emailService).emails.size()); (6)
}
}
1 | Annotate the class with @MicronatTest to let Micronaut starts the embedded server and inject the beans. More info: https://micronaut-projects.github.io/micronaut-test/latest/guide/index.html |
2 | Define a property used to run the test |
3 | Inject the ApplicationContext bean |
4 | Inject the RxHttpClient bean |
5 | Creating HTTP Requests is easy thanks to Micronaut’s fluid API. |
6 | emailService.send method is invoked once. |
4.5. Validation
We want to ensure any email request contains a subject, recipient and a text body or html body.
Micronaut’s validation is built on the standard framework – JSR 380, also known as Bean Validation 2.0.
Hibernate Validator is a reference implementation of the validation API. Starting with Micronaut 1.2, Micronaut
has built-in support for validation of beans that
are annotated with javax.validation
annotations.
The necessary dependencies are included by default when creating a new application, so you don’t need to add anything else.
Create the next test:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Property;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import javax.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest (1)
@Property(name = "spec.name", value = "mailcontroller") (2)
class MailControllerValidationTest {
@Inject
ApplicationContext applicationContext;
@Inject
@Client("/")
RxHttpClient client;
@Test
public void mailSendCannotBeInvokedWithoubSubject() {
EmailCmd cmd = new EmailCmd();
cmd.setRecipient("delamos@micronaut.example");
cmd.setTextBody("Hola hola");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
Executable e = () -> client.toBlocking().exchange(request);
HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
assertEquals(HttpStatus.BAD_REQUEST, thrown.getStatus());
}
@Test
public void mailSendCannotBeInvokedWithoutRecipient() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setTextBody("Hola hola");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
Executable e = () -> client.toBlocking().exchange(request);
HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
assertEquals(HttpStatus.BAD_REQUEST, thrown.getStatus());
}
@Test
public void mailSendCannotBeInvokedWithoutEitherTextBodyOrHtmlBody() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setRecipient("delamos@micronaut.example");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
Executable e = () -> client.toBlocking().exchange(request);
HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
assertEquals(HttpStatus.BAD_REQUEST, thrown.getStatus());
}
@Test
public void mailSendCanBeInvokedWithoutTextBodyAndNotHtmlBody() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setRecipient("delamos@micronaut.example");
cmd.setTextBody("Hello");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
HttpResponse<?> rsp = client.toBlocking().exchange(request);
assertEquals(HttpStatus.OK, rsp.getStatus());
}
@Test
public void mailSendCanBeInvokedWithoutHtmlBodyAndNotTextBody() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setRecipient("delamos@micronaut.example");
cmd.setHtmlBody("<h1>Hello</h1>");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
HttpResponse<?> rsp = client.toBlocking().exchange(request);
assertEquals(HttpStatus.OK, rsp.getStatus());
}
}
1 | Annotate the class with @MicronatTest to let Micronaut starts the embedded server and inject the beans. More info: https://micronaut-projects.github.io/micronaut-test/latest/guide/index.html. |
2 | Define a property available for the application. |
3 | Creating HTTP Requests is easy thanks to Micronaut’s fluid API. |
In order to satisfy the test, create an email constraints annotation
package example.micronaut;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Constraint(validatedBy = {})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailConstraints {
String message() default "{email.invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and a constraint validator in a @Factory
class:
package example.micronaut;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.util.StringUtils;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import javax.inject.Singleton;
@Factory
public class EmailConstraintsFactory {
@Singleton
ConstraintValidator<EmailConstraints, EmailCmd> emailBodyValidator() {
return (value, annotationMetadata, context) ->
value != null &&
(StringUtils.isNotEmpty(value.getTextBody()) || StringUtils.isNotEmpty(value.getHtmlBody()));
}
}
Annotate EmailCmd
with EmailConstraints
and @Introspected
(to generate the
Bean Introspection information).
@EmailConstraints
@Introspected
public class EmailCmd implements Email {
@NotNull
@NotBlank
private String recipient;
@NotNull
@NotBlank
private String subject;
private List<String> cc = new ArrayList<>();
private List<String> bcc = new ArrayList<>();
private String htmlBody;
private String textBody;
private String replyTo;
// getters & setters
}
5. Testing the Application
To run the tests:
$ ./mvnw test
6. Next steps
Explore more features with Micronaut Guides.
7. Help with Micronaut
Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.