FluentEmailAssert.java

package fr.sii.ogham.testing.assertion.email;

import static fr.sii.ogham.testing.assertion.util.AssertionHelper.assertThat;
import static fr.sii.ogham.testing.assertion.util.AssertionHelper.usingContext;
import static fr.sii.ogham.testing.assertion.util.EmailUtils.getAlternativePart;
import static fr.sii.ogham.testing.assertion.util.EmailUtils.getAttachments;
import static fr.sii.ogham.testing.assertion.util.EmailUtils.getBodyPart;
import static java.util.Arrays.asList;
import static javax.mail.Message.RecipientType.CC;
import static javax.mail.Message.RecipientType.TO;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.lessThanOrEqualTo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;

import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.internet.InternetAddress;

import org.hamcrest.Matcher;

import fr.sii.ogham.testing.assertion.context.SingleMessageContext;
import fr.sii.ogham.testing.assertion.filter.FileNamePredicate;
import fr.sii.ogham.testing.assertion.util.AssertionRegistry;
import fr.sii.ogham.testing.assertion.util.EmailUtils;
import fr.sii.ogham.testing.util.HasParent;

@SuppressWarnings("squid:S1192")
public class FluentEmailAssert<P> extends HasParent<P> {
	/**
	 * The list of messages that will be used for assertions
	 */
	private final List<? extends Message> actual;
	private int index;
	/**
	 * Registry to register assertions
	 */
	private final AssertionRegistry registry;

	public FluentEmailAssert(Message actual, int index, P parent, AssertionRegistry registry) {
		this(Arrays.asList(actual), parent, registry);
		this.index = index;
	}

	public FluentEmailAssert(List<? extends Message> actual, P parent, AssertionRegistry registry) {
		super(parent);
		this.actual = actual;
		this.registry = registry;
	}

	/**
	 * Make assertions on the subject of the message(s).
	 * 
	 * <pre>
	 * .receivedMessages().message(0)
	 *    .subject(is("foobar"))
	 * </pre>
	 * 
	 * Will check if the subject of the first message is exactly "foobar".
	 * 
	 * <pre>
	 * .receivedMessages().every()
	 *    .subject(is("foobar"))
	 * </pre>
	 * 
	 * Will check that the subject of every message is exactly "foobar".
	 * 
	 * @param matcher
	 *            the assertion to apply on subject
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentEmailAssert<P> subject(Matcher<? super String> matcher) {
		try {
			String desc = "subject of message ${messageIndex}";
			int msgIdx = this.index;
			for (Message message : actual) {
				final int idx = msgIdx;
				registry.register(() -> assertThat(message.getSubject(), usingContext(desc, new SingleMessageContext(idx), matcher)));
				msgIdx++;
			}
			return this;
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get subject of messsage", e);
		}
	}

	/**
	 * Make assertions on the body of the message(s) using fluent API. The body
	 * of the message is either:
	 * <ul>
	 * <li>The part of the message when it contains only one part</li>
	 * <li>The part with text or HTML mimetype if only one part with one of that
	 * mimetype</li>
	 * <li>The second part with text or HTML mimetype if there are two text or
	 * HTML parts</li>
	 * </ul>
	 * 
	 * <pre>
	 * .receivedMessages().message(0).body()
	 *    .contentAsString(is("email body"))
	 *    .contentType(is("text/plain"))
	 * </pre>
	 * 
	 * Will check if the body content of the first message is "email body" and
	 * content-type of the first message is "text/plain".
	 * 
	 * <pre>
	 * .receivedMessages().every().body()
	 *    .contentAsString(is("email body"))
	 *    .contentType(is("text/plain"))
	 * </pre>
	 * 
	 * Will check that the body content of every message is "email body" and
	 * content-type of every message is "text/plain".
	 * 
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentPartAssert<FluentEmailAssert<P>> body() {
		try {
			int msgIdx = this.index;
			List<PartWithContext> bodies = new ArrayList<>();
			for (Message message : actual) {
				bodies.add(new PartWithContext(getBodyPart(message), "body", new SingleMessageContext(msgIdx)));
				msgIdx++;
			}
			return new FluentPartAssert<>(bodies, this, registry);
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get body of messsage", e);
		}
	}

	/**
	 * Make assertions on the alternative part of the message(s) using fluent
	 * API. The alternative is useful when sending HTML email that may be
	 * unreadable on some email clients. For example, a smartphone will display
	 * the 2 or 3 first lines as a summary. Many smartphones will take the HTML
	 * message as-is and will display HTML tags instead of content of email.
	 * Alternative is used to provide a textual visualization of the message
	 * that will be readable by any system.
	 * 
	 * <p>
	 * The alternative of the message is either:
	 * <ul>
	 * <li>null if there is only one part</li>
	 * <li>null if there is only one text or HTML part</li>
	 * <li>the first part if there are more than one text or HTML part</li>
	 * </ul>
	 * 
	 * <pre>
	 * .receivedMessages().message(0).alternative()
	 *    .contentAsString(is("email alternative"))
	 *    .contentType(is("text/plain"))
	 * </pre>
	 * 
	 * Will check if the body content of the first message is "email
	 * alternative" and content-type of the first message is "text/plain".
	 * 
	 * <pre>
	 * .receivedMessages().every().alternative()
	 *    .contentAsString(is("email alternative"))
	 *    .contentType(is("text/plain"))
	 * </pre>
	 * 
	 * Will check that the body content of every message is "email alternative"
	 * and content-type of every message is "text/plain".
	 * 
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentPartAssert<FluentEmailAssert<P>> alternative() {
		try {
			int msgIdx = this.index;
			List<PartWithContext> bodies = new ArrayList<>();
			for (Message message : actual) {
				bodies.add(new PartWithContext(getAlternativePart(message), "alternative", new SingleMessageContext(msgIdx)));
				msgIdx++;
			}
			return new FluentPartAssert<>(bodies, this, registry);
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get body of messsage", e);
		}
	}

	/**
	 * Make assertions on the body of the message(s). The body of the message is
	 * either:
	 * <ul>
	 * <li>The part of the message when it contains only one part</li>
	 * <li>The part with text or HTML mimetype if only one part with one of that
	 * mimetype</li>
	 * <li>The second part with text or HTML mimetype if there are two text or
	 * HTML parts</li>
	 * </ul>
	 * 
	 * <pre>
	 * .receivedMessages().message(0)
	 *    .body(allOf(notNullValue(), instanceOf(MimeBodyPart.class))
	 * </pre>
	 * 
	 * Will check if the body of the first message is not null and is a
	 * MimeBodyPart instance.
	 * 
	 * <pre>
	 * .receivedMessages().every()
	 *    .body(allOf(notNullValue(), instanceOf(MimeBodyPart.class))
	 * </pre>
	 * 
	 * Will check that the body of every message is not null and is a
	 * MimeBodyPart instance.
	 * 
	 * <p>
	 * You can use the {@link #body()} variant to make more powerful assertions.
	 * 
	 * @param matcher
	 *            the assertion to apply on body
	 * @param <T>
	 *            the type used for the matcher
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public <T extends Part> FluentEmailAssert<P> body(Matcher<? super Part> matcher) { // NOSONAR
		try {
			String desc = "body of message ${messageIndex}";
			int msgIdx = this.index;
			for (Message message : actual) {
				final int idx = msgIdx;
				registry.register(() -> assertThat(getBodyPart(message), usingContext(desc, new SingleMessageContext(idx), matcher)));
				msgIdx++;
			}
			return this;
		} catch (MessagingException e) {
			throw new AssertionError("Failed to access attachments of messsage", e);
		}
	}

	/**
	 * Make assertions on the alternative part of the message(s). The
	 * alternative is useful when sending HTML email that may be unreadable on
	 * some email clients. For example, a smartphone will display the 2 or 3
	 * first lines as a summary. Many smartphones will take the HTML message
	 * as-is and will display HTML tags instead of content of email. Alternative
	 * is used to provide a textual visualization of the message that will be
	 * readable by any system.
	 * 
	 * <p>
	 * The alternative of the message is either:
	 * <ul>
	 * <li>null if there is only one part</li>
	 * <li>null if there is only one text or HTML part</li>
	 * <li>the first part if there are more than one text or HTML part</li>
	 * </ul>
	 * 
	 * <pre>
	 * .receivedMessages().message(0)
	 *    .alternative(allOf(notNullValue(), instanceOf(MimeBodyPart.class))
	 * </pre>
	 * 
	 * Will check if the alternative of the first message is not null and is a
	 * MimeBodyPart instance.
	 * 
	 * <pre>
	 * .receivedMessages().every()
	 *    .alternative(allOf(notNullValue(), instanceOf(MimeBodyPart.class))
	 * </pre>
	 * 
	 * Will check that the alternative of every message is not null and is a
	 * MimeBodyPart instance.
	 * 
	 * <p>
	 * You can use the {@link #alternative()} variant to make more powerful
	 * assertions.
	 * 
	 * @param matcher
	 *            the assertion to apply on alternative
	 * @param <T>
	 *            the type used for the matcher
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public <T extends Part> FluentEmailAssert<P> alternative(Matcher<? super Part> matcher) { // NOSONAR
		try {
			String desc = "alternative of message ${messageIndex}";
			int msgIdx = this.index;
			for (Message message : actual) {
				final int idx = msgIdx;
				registry.register(() -> assertThat(getAlternativePart(message), usingContext(desc, new SingleMessageContext(idx), matcher)));
				msgIdx++;
			}
			return this;
		} catch (MessagingException e) {
			throw new AssertionError("Failed to access attachments of messsage", e);
		}
	}

	/**
	 * Make assertions on the sender of the message(s) using fluent API.
	 * 
	 * <pre>
	 * .receivedMessages().message(0).from()
	 *    .address(hasItems("noreply@sii.fr"))
	 *    .personal(hasItems("Groupe SII"))
	 * </pre>
	 * 
	 * Will check if the sender email address of the first message is exactly
	 * "noreply@sii.fr" and sender displayed address of the first message is
	 * exactly "Groupe SII".
	 * 
	 * <pre>
	 * .receivedMessages().every().from()
	 *    .address(hasItems("noreply@sii.fr"))
	 *    .personal(hasItems("Groupe SII"))
	 * </pre>
	 * 
	 * Will check if the sender email address of every message is exactly
	 * "noreply@sii.fr" and sender displayed address of every message is exactly
	 * "Groupe SII".
	 * 
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentAddressListAssert<FluentEmailAssert<P>> from() {
		try {
			int msgIdx = this.index;
			List<AddressesWithContext> addresses = new ArrayList<>();
			for (Message message : actual) {
				addresses.add(new AddressesWithContext(asList((InternetAddress[]) message.getFrom()), "from", new SingleMessageContext(msgIdx)));
				msgIdx++;
			}
			return new FluentAddressListAssert<>(addresses, this, registry);
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get from field of messsage", e);
		}
	}

	/**
	 * Make assertions on the sender of the message(s) using fluent API.
	 * 
	 * <pre>
	 * .receivedMessages().message(0).to()
	 *    .address(hasItems("recipient1@sii.fr", "recipient2@sii.fr"))
	 *    .personal(hasItems("Foo", "Bar"))
	 * </pre>
	 * 
	 * Will check if the list of email addresses of direct recipients (TO) of
	 * the first message are exactly "recipient1@sii.fr", "recipient2@sii.fr"
	 * and the list of displayed address of direct recipients (TO) of the first
	 * message are exactly "Foo", "Bar".
	 * 
	 * <pre>
	 * .receivedMessages().every().to()
	 *    .address(hasItems("recipient1@sii.fr", "recipient2@sii.fr"))
	 *    .personal(hasItems("Foo", "Bar"))
	 * </pre>
	 * 
	 * Will check if the list of email addresses of direct recipients (TO) of
	 * every message are exactly "recipient1@sii.fr", "recipient2@sii.fr" and
	 * the list of displayed address of direct recipients (TO) of every message
	 * are exactly "Foo", "Bar".
	 * 
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentAddressListAssert<FluentEmailAssert<P>> to() {
		try {
			int msgIdx = this.index;
			List<AddressesWithContext> addresses = new ArrayList<>();
			for (Message message : actual) {
				addresses.add(new AddressesWithContext(asList((InternetAddress[]) message.getRecipients(TO)), "to", new SingleMessageContext(msgIdx)));
				msgIdx++;
			}
			return new FluentAddressListAssert<>(addresses, this, registry);
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get to field of messsage", e);
		}
	}

	/**
	 * Make assertions on the sender of the message(s) using fluent API.
	 * 
	 * <pre>
	 * .receivedMessages().message(0).cc()
	 *    .address(hasItems("recipient1@sii.fr", "recipient2@sii.fr"))
	 *    .personal(hasItems("Foo", "Bar"))
	 * </pre>
	 * 
	 * Will check if the list of email addresses of copy recipients (CC) of the
	 * first message are exactly "recipient1@sii.fr", "recipient2@sii.fr" and
	 * the list of displayed address of copy recipients (CC) of the first
	 * message are exactly "Foo", "Bar".
	 * 
	 * <pre>
	 * .receivedMessages().every().cc()
	 *    .address(hasItems("recipient1@sii.fr", "recipient2@sii.fr"))
	 *    .personal(hasItems("Foo", "Bar"))
	 * </pre>
	 * 
	 * Will check if the list of email addresses of copy recipients (CC) of
	 * every message are exactly "recipient1@sii.fr", "recipient2@sii.fr" and
	 * the list of displayed address of copy recipients (CC) of every message
	 * are exactly "Foo", "Bar".
	 * 
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentAddressListAssert<FluentEmailAssert<P>> cc() {
		try {
			int msgIdx = this.index;
			List<AddressesWithContext> addresses = new ArrayList<>();
			for (Message message : actual) {
				addresses.add(new AddressesWithContext(asList((InternetAddress[]) message.getRecipients(CC)), "cc", new SingleMessageContext(msgIdx)));
				msgIdx++;
			}
			return new FluentAddressListAssert<>(addresses, this, registry);
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get cc field of messsage", e);
		}
	}

	/**
	 * Make assertions on the list of attachments of the message(s).
	 * 
	 * <pre>
	 * .receivedMessages().message(0)
	 *    .attachments(hasSize(1))
	 * </pre>
	 * 
	 * Will check if the number of attachments of the first message is exactly
	 * 1.
	 * 
	 * <pre>
	 * .receivedMessages().every()
	 *    .attachments(hasSize(1))
	 * </pre>
	 * 
	 * Will check that the number of attachments of every message is exactly 1.
	 * 
	 * <p>
	 * You can use the {@link #attachment(String)} or
	 * {@link #attachments(Predicate)} variants to make more powerful assertions
	 * on a particular attachment.
	 * 
	 * @param matcher
	 *            the assertion to apply on list of attachments
	 * @param <T>
	 *            the type used for the matcher
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public <T extends Collection<? extends BodyPart>> FluentEmailAssert<P> attachments(Matcher<? super Collection<? extends BodyPart>> matcher) { // NOSONAR
		try {
			String desc = "attachments of message ${messageIndex}";
			int msgIdx = this.index;
			for (Message message : actual) {
				final int idx = msgIdx;
				registry.register(() -> assertThat(getAttachments(message), usingContext(desc, new SingleMessageContext(idx), matcher)));
				msgIdx++;
			}
			return this;
		} catch (MessagingException e) {
			throw new AssertionError("Failed to access attachments of messsage", e);
		}
	}

	/**
	 * Make assertions on a particular attachment of the message(s) using fluent
	 * API. The attachment is identified by its filename.
	 * 
	 * <pre>
	 * .receivedMessages().message(0).attachment("foo.pdf")
	 *    .contentType(is("application/pdf"))
	 * </pre>
	 * 
	 * Will check if the content-type of the attachment named "foo.pdf" of the
	 * first message is exactly "application/pdf".
	 * 
	 * <pre>
	 * .receivedMessages().every().attachment("foo.pdf")
	 *    .contentType(is("application/pdf"))
	 * </pre>
	 * 
	 * Will check that the content-type of attachment named "foo.pdf" of every
	 * message is exactly "application/pdf".
	 * 
	 * <p>
	 * This is a shortcut to {@link #attachments(Predicate)} with
	 * {@link FileNamePredicate};
	 * 
	 * @param filename
	 *            the name of the attachment to make assertions on it
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentPartAssert<FluentEmailAssert<P>> attachment(String filename) {
		return attachment(By.filename(filename));
	}

	/**
	 * Make assertions on a particular attachment of the message(s).
	 * 
	 * <pre>
	 * .receivedMessages().message(0).attachment(0)
	 *    .contentType(is("application/pdf"))
	 * </pre>
	 * 
	 * Will check if the content-type of the first attachment of the first
	 * message is exactly "application/pdf".
	 * 
	 * <pre>
	 * .receivedMessages().every().attachment(0)
	 *    .contentType(is("application/pdf"))
	 * </pre>
	 * 
	 * Will check if the content-type of the first attachment of every message
	 * is exactly "application/pdf".
	 * 
	 * @param index
	 *            the index of the attachment
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentPartAssert<FluentEmailAssert<P>> attachment(int index) {
		try {
			int msgIndex = this.index;
			List<PartWithContext> attachments = new ArrayList<>();
			for (Message message : actual) {
				List<BodyPart> found = getAttachments(message);
				BodyPart attachment = index >= found.size() ? null : found.get(index);
				attachments.add(new PartWithContext(attachment, "attachment with index " + index + (attachment == null ? " (/!\\ not found)" : ""), new SingleMessageContext(msgIndex)));
				msgIndex++;
			}
			return new FluentPartAssert<>(attachments, this, registry);
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get attachment with index " + index + " of messsage", e);
		}
	}

	/**
	 * Make assertions on a particular attachment of the message(s) using fluent
	 * API. The attachment is identified using the provided finder method.
	 * 
	 * <p>
	 * If several attachments are found, it fails. If you want to make the same
	 * assertions on several attachments at once, use {@link #attachments(By)}
	 * instead.
	 * 
	 * <pre>
	 * .receivedMessages().message(0).attachment(By.filename("foo.pdf"))
	 *    .contentType(is("application/pdf"))
	 * </pre>
	 * 
	 * Will check if the content-type of the attachment named "foo.pdf" of the
	 * first message is exactly "application/pdf".
	 * 
	 * <pre>
	 * .receivedMessages().every().attachment(By.filename("foo.pdf"))
	 *    .contentType(is("application/pdf"))
	 * </pre>
	 * 
	 * Will check that the content-type of attachment named "foo.pdf" of every
	 * message is exactly "application/pdf".
	 * 
	 * <p>
	 * This is a shortcut to {@link #attachments(Predicate)} with
	 * {@link FileNamePredicate};
	 * 
	 * @param by
	 *            the finder method
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentPartAssert<FluentEmailAssert<P>> attachment(By by) {
		return attachments(by.toPredicate(), hasSize(lessThanOrEqualTo(1)));
	}

	/**
	 * Make assertions on a one or several attachments of the message(s) using
	 * fluent API. The attachments are identified using provided finder method.
	 * 
	 * <pre>
	 * .receivedMessages().message(0)
	 *    .attachments(By.contentId("cid2")).filename(endsWith(".pdf"))
	 * </pre>
	 * 
	 * Will check if every attachment that have the Content-ID header set to
	 * "cid2" of first message has a name ending with ".pdf".
	 * 
	 * <pre>
	 * .receivedMessages().every()
	 *    .attachments(By.contentId("cid2")).filename(endsWith(".pdf"))
	 * </pre>
	 * 
	 * Will check if every attachment that have the Content-ID header set to
	 * "cid2" of every messages has a name ending with ".pdf".
	 * 
	 * 
	 * @param by
	 *            the finder method
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentPartAssert<FluentEmailAssert<P>> attachments(By by) {
		return attachments(by.toPredicate());
	}

	/**
	 * Make assertions on a one or several attachments of the message(s) using
	 * fluent API. The attachments are identified using provided predicate.
	 * 
	 * <pre>
	 * .receivedMessages().message(0)
	 *    .attachments(new PdfFilter()).filename(endsWith(".pdf"))
	 * </pre>
	 * 
	 * Will check if the name of every PDF attachments of the first message are
	 * named "foo.pdf".
	 * 
	 * <pre>
	 * .receivedMessages().every()
	 *    .attachments(new PdfFilter()).filename(endsWith(".pdf"))
	 * </pre>
	 * 
	 * Will check if the name of every PDF attachments of every message are
	 * named "foo.pdf".
	 * 
	 * 
	 * @param filter
	 *            the filter used to find attachments
	 * @return the fluent API for chaining assertions on received message(s)
	 */
	public FluentPartAssert<FluentEmailAssert<P>> attachments(Predicate<Part> filter) {
		return attachments(filter, null);
	}
	
	private FluentPartAssert<FluentEmailAssert<P>> attachments(Predicate<Part> filter, Matcher<? super Collection<? extends Part>> singleMessageAttachmentsMatcher) {
		try {
			int msgIdx = this.index;
			List<PartWithContext> attachments = new ArrayList<>();
			for (Message message : actual) {
				int matchingIdx = 0;
				boolean noneFound = true;
				List<Part> matchingAttachmentsForMessage = EmailUtils.<Part> getAttachments(message, filter);
				checkFoundAttachmentPerMessage(filter, singleMessageAttachmentsMatcher, msgIdx, matchingAttachmentsForMessage);
				contextualize(filter, msgIdx, attachments, matchingIdx, noneFound, matchingAttachmentsForMessage);
				msgIdx++;
			}
			return new FluentPartAssert<>(attachments, this, registry);
		} catch (MessagingException e) {
			throw new AssertionError("Failed to get attachment " + filter + " of messsage", e);
		}
	}

	private static void contextualize(Predicate<Part> filter, int msgIdx, List<PartWithContext> attachments, int matchingIdx, boolean noneFound, List<Part> matchingAttachmentsForMessage) {
		for (Part attachment : matchingAttachmentsForMessage) {
			noneFound = false;
			attachments.add(new PartWithContext(attachment, "attachment " + filter + " (matching index: " + matchingIdx + ")", new SingleMessageContext(msgIdx)));
			matchingIdx++;
		}
		if (noneFound) {
			attachments.add(new PartWithContext(null, "attachment " + filter + " (/!\\ not found)", new SingleMessageContext(msgIdx)));
		}
	}

	private void checkFoundAttachmentPerMessage(Predicate<Part> filter, Matcher<? super Collection<? extends Part>> singleMessageAttachmentsMatcher, int msgIdx,
			List<Part> matchingAttachmentsForMessage) {
		if (singleMessageAttachmentsMatcher != null) {
			registry.register(() -> assertThat("message "+msgIdx+" should have only one attachment "+filter, matchingAttachmentsForMessage, singleMessageAttachmentsMatcher));
		}
	}
}