org.apache.james.mailrepository.MailRepositoryContract.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.james.mailrepository.MailRepositoryContract.java

Source

/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 ****************************************************************/

package org.apache.james.mailrepository;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.IntStream;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.apache.james.core.MailAddress;
import org.apache.james.core.MaybeSender;
import org.apache.james.core.builder.MimeMessageBuilder;
import org.apache.james.mailrepository.api.MailKey;
import org.apache.james.mailrepository.api.MailRepository;
import org.apache.james.server.core.MailImpl;
import org.apache.james.util.concurrency.ConcurrentTestRunner;
import org.apache.james.utils.DiscreteDistribution;
import org.apache.james.utils.DiscreteDistribution.DistributionEntry;
import org.apache.mailet.Mail;
import org.apache.mailet.PerRecipientHeaders;
import org.apache.mailet.base.MailAddressFixture;
import org.apache.mailet.base.test.FakeMail;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;

import com.github.fge.lambdas.Throwing;
import com.github.fge.lambdas.runnable.ThrowingRunnable;
import com.github.steveash.guavate.Guavate;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.hash.Hashing;

public interface MailRepositoryContract {

    String TEST_ATTRIBUTE = "testAttribute";
    MailKey MAIL_1 = new MailKey("mail1");
    MailKey MAIL_2 = new MailKey("mail2");
    MailKey UNKNOWN_KEY = new MailKey("random");

    default MailImpl createMail(MailKey key) throws MessagingException {
        return createMail(key, "original body");
    }

    default MailImpl createMail(MailKey key, String body) throws MessagingException {
        MimeMessage mailContent = generateMailContent(body);
        List<MailAddress> recipients = ImmutableList.of(new MailAddress("rec1@domain.com"),
                new MailAddress("rec2@domain.com"));
        MailAddress sender = new MailAddress("sender@domain.com");
        MailImpl mail = new MailImpl(key.asString(), sender, recipients, mailContent);
        mail.setAttribute(TEST_ATTRIBUTE, "testValue");
        return mail;
    }

    default MimeMessage generateMailContent(String body) throws MessagingException {
        return MimeMessageBuilder.mimeMessageBuilder().setSubject("test").setText(body).build();
    }

    default void checkMailEquality(Mail actual, Mail expected) {
        assertSoftly(Throwing.consumer(softly -> {
            softly.assertThat(actual.getMessage().getContent()).isEqualTo(expected.getMessage().getContent());
            softly.assertThat(actual.getMessageSize()).isEqualTo(expected.getMessageSize());
            softly.assertThat(actual.getName()).isEqualTo(expected.getName());
            softly.assertThat(actual.getState()).isEqualTo(expected.getState());
            softly.assertThat(actual.getAttribute(TEST_ATTRIBUTE)).isEqualTo(expected.getAttribute(TEST_ATTRIBUTE));
            softly.assertThat(actual.getErrorMessage()).isEqualTo(expected.getErrorMessage());
            softly.assertThat(actual.getRemoteHost()).isEqualTo(expected.getRemoteHost());
            softly.assertThat(actual.getRemoteAddr()).isEqualTo(expected.getRemoteAddr());
            softly.assertThat(actual.getLastUpdated()).isEqualTo(expected.getLastUpdated());
            softly.assertThat(actual.getPerRecipientSpecificHeaders())
                    .isEqualTo(expected.getPerRecipientSpecificHeaders());
        }));
    }

    MailRepository retrieveRepository() throws Exception;

    @Test
    default void sizeShouldReturnZeroWhenEmpty() throws Exception {
        MailRepository testee = retrieveRepository();
        assertThat(testee.size()).isEqualTo(0L);
    }

    @Test
    default void sizeShouldReturnMailCount() throws Exception {
        MailRepository testee = retrieveRepository();

        testee.store(createMail(MAIL_1));
        testee.store(createMail(MAIL_2));

        assertThat(testee.size()).isEqualTo(2L);
    }

    @Test
    default void sizeShouldBeIncrementedByOneWhenDuplicates() throws Exception {
        MailRepository testee = retrieveRepository();

        testee.store(createMail(MAIL_1));
        testee.store(createMail(MAIL_1));

        assertThat(testee.size()).isEqualTo(1L);
    }

    @Test
    default void sizeShouldBeDecrementedByRemove() throws Exception {
        MailRepository testee = retrieveRepository();

        testee.store(createMail(MAIL_1));
        testee.remove(MAIL_1);

        assertThat(testee.size()).isEqualTo(0L);
    }

    @Test
    default void storeRegularMailShouldNotFailWhenNullSender() throws Exception {
        MailRepository testee = retrieveRepository();
        Mail mail = FakeMail.builder().sender(MailAddress.nullSender()).recipient(MailAddressFixture.RECIPIENT1)
                .name(MAIL_1.asString()).mimeMessage(generateMailContent("String body")).build();

        testee.store(mail);

        assertThat(testee.retrieve(MAIL_1).getMaybeSender()).isEqualTo(MaybeSender.nullSender());
    }

    @Test
    default void storeRegularMailShouldNotFail() throws Exception {
        MailRepository testee = retrieveRepository();
        Mail mail = createMail(MAIL_1);

        testee.store(mail);
    }

    @Test
    default void storeBigMailShouldNotFail() throws Exception {
        MailRepository testee = retrieveRepository();
        String bigString = Strings.repeat("my mail is big ?", 1_000_000);
        Mail mail = createMail(MAIL_1, bigString);

        testee.store(mail);
    }

    @Test
    default void retrieveShouldGetStoredMail() throws Exception {
        MailRepository testee = retrieveRepository();
        Mail mail = createMail(MAIL_1);

        testee.store(mail);

        assertThat(testee.retrieve(MAIL_1)).satisfies(actual -> checkMailEquality(actual, mail));
    }

    @Test
    default void removeAllShouldRemoveStoredMails() throws Exception {
        MailRepository testee = retrieveRepository();
        testee.store(createMail(MAIL_1));

        testee.removeAll();

        assertThat(testee.size()).isEqualTo(0L);
    }

    @Test
    default void retrieveShouldReturnNullAfterRemoveAll() throws Exception {
        MailRepository testee = retrieveRepository();
        testee.store(createMail(MAIL_1));

        testee.removeAll();

        assertThat(testee.retrieve(MAIL_1)).isNull();
    }

    @Test
    default void removeAllShouldBeIdempotent() throws Exception {
        MailRepository testee = retrieveRepository();
        testee.store(createMail(MAIL_1));

        testee.removeAll();
        testee.removeAll();

        assertThat(testee.size()).isEqualTo(0L);
    }

    @Test
    default void removeAllShouldNotFailWhenEmpty() throws Exception {
        MailRepository testee = retrieveRepository();

        testee.removeAll();
    }

    @Test
    default void retrieveShouldGetStoredEmojiMail() throws Exception {
        MailRepository testee = retrieveRepository();
        Mail mail = createMail(MAIL_1, "my content contains ?");

        testee.store(mail);

        assertThat(testee.retrieve(MAIL_1).getMessage().getContent()).isEqualTo("my content contains ?");
    }

    @Test
    default void retrieveBigMailShouldHaveSameHash() throws Exception {
        MailRepository testee = retrieveRepository();
        String bigString = Strings.repeat("my mail is big ?", 1_000_000);
        Mail mail = createMail(MAIL_1, bigString);
        testee.store(mail);

        Mail actual = testee.retrieve(MAIL_1);

        assertThat(Hashing.sha256().hashString((String) actual.getMessage().getContent(), StandardCharsets.UTF_8))
                .isEqualTo(Hashing.sha256().hashString(bigString, StandardCharsets.UTF_8));
    }

    @Test
    default void retrieveShouldReturnAllMailProperties() throws Exception {
        MailRepository testee = retrieveRepository();
        MailImpl mail = createMail(MAIL_1);
        mail.setErrorMessage("Error message");
        mail.setRemoteAddr("172.5.2.3");
        mail.setRemoteHost("smtp@domain.com");
        mail.setLastUpdated(new Date());
        mail.addSpecificHeaderForRecipient(PerRecipientHeaders.Header.builder().name("name").value("value").build(),
                new MailAddress("bob@domain.com"));

        testee.store(mail);

        assertThat(testee.retrieve(MAIL_1)).satisfies(actual -> checkMailEquality(actual, mail));
    }

    @Test
    default void newlyCreatedRepositoryShouldNotContainAnyMail() throws Exception {
        MailRepository testee = retrieveRepository();

        assertThat(testee.list()).isEmpty();
    }

    @Test
    default void retrievingUnknownMailShouldReturnNull() throws Exception {
        MailRepository testee = retrieveRepository();

        assertThat(testee.retrieve(UNKNOWN_KEY)).isNull();
    }

    @Test
    default void removingUnknownMailShouldHaveNoEffect() throws Exception {
        MailRepository testee = retrieveRepository();

        testee.remove(UNKNOWN_KEY);
    }

    @Test
    default void retrieveShouldReturnNullWhenKeyWasRemoved() throws Exception {
        MailRepository testee = retrieveRepository();
        testee.store(createMail(MAIL_1));

        testee.remove(MAIL_1);

        assertThat(retrieveRepository().list()).doesNotContain(MAIL_1);
        assertThat(retrieveRepository().retrieve(MAIL_1)).isNull();
    }

    @Test
    default void removeShouldnotAffectUnrelatedMails() throws Exception {
        MailRepository testee = retrieveRepository();
        testee.store(createMail(MAIL_1));
        testee.store(createMail(MAIL_2));

        testee.remove(MAIL_1);

        assertThat(retrieveRepository().list()).contains(MAIL_2);
    }

    @Test
    default void removedMailsShouldNotBeListed() throws Exception {
        MailRepository testee = retrieveRepository();

        MailKey key3 = new MailKey("mail3");
        Mail mail1 = createMail(MAIL_1);
        Mail mail2 = createMail(MAIL_2);
        Mail mail3 = createMail(key3);
        retrieveRepository().store(mail1);
        retrieveRepository().store(mail2);
        retrieveRepository().store(mail3);

        testee.remove(ImmutableList.of(mail1, mail3));

        assertThat(retrieveRepository().list()).contains(MAIL_2).doesNotContain(MAIL_1, key3);
    }

    @Test
    default void removedMailShouldNotBeListed() throws Exception {
        MailRepository testee = retrieveRepository();

        MailKey key3 = new MailKey("mail3");
        Mail mail1 = createMail(MAIL_1);
        Mail mail2 = createMail(MAIL_2);
        Mail mail3 = createMail(key3);
        retrieveRepository().store(mail1);
        retrieveRepository().store(mail2);
        retrieveRepository().store(mail3);

        testee.remove(mail2);

        assertThat(retrieveRepository().list()).contains(MAIL_1, key3).doesNotContain(MAIL_2);
    }

    @Test
    default void removeShouldHaveNoEffectForUnknownMails() throws Exception {
        MailRepository testee = retrieveRepository();

        testee.remove(ImmutableList.of(createMail(UNKNOWN_KEY)));

        assertThat(retrieveRepository().list()).isEmpty();
    }

    @Test
    default void removeShouldHaveNoEffectOnSizeWhenUnknownKeys() throws Exception {
        MailRepository testee = retrieveRepository();

        Mail mail1 = createMail(MAIL_1);
        testee.store(mail1);

        testee.remove(ImmutableList.of(createMail(UNKNOWN_KEY)));

        assertThat(testee.size()).isEqualTo(1);
    }

    @Test
    default void storeShouldHaveNoEffectOnSizeWhenAlreadyStoredMail() throws Exception {
        MailRepository testee = retrieveRepository();

        Mail mail1 = createMail(MAIL_1);
        testee.store(mail1);
        testee.store(mail1);

        assertThat(testee.size()).isEqualTo(1);
    }

    @Test
    default void removeShouldHaveNoEffectForUnknownMail() throws Exception {
        MailRepository testee = retrieveRepository();

        testee.remove(createMail(UNKNOWN_KEY));

        assertThat(retrieveRepository().list()).isEmpty();
    }

    @Test
    default void listShouldReturnStoredMailsKeys() throws Exception {
        MailRepository testee = retrieveRepository();
        testee.store(createMail(MAIL_1));

        testee.store(createMail(MAIL_2));

        assertThat(testee.list()).containsOnly(MAIL_1, MAIL_2);
    }

    @Test
    default void storingMessageWithSameKeyTwiceShouldUpdateMessageContent() throws Exception {
        MailRepository testee = retrieveRepository();
        testee.store(createMail(MAIL_1));

        Mail updatedMail = createMail(MAIL_1, "modified content");
        testee.store(updatedMail);

        assertThat(testee.list()).hasSize(1);
        assertThat(testee.retrieve(MAIL_1)).satisfies(actual -> checkMailEquality(actual, updatedMail));
    }

    @Test
    default void storingMessageWithSameKeyTwiceShouldUpdateMessageAttributes() throws Exception {
        MailRepository testee = retrieveRepository();
        Mail mail = createMail(MAIL_1);
        testee.store(mail);

        mail.setAttribute(TEST_ATTRIBUTE, "newValue");
        testee.store(mail);

        assertThat(testee.list()).hasSize(1);
        assertThat(testee.retrieve(MAIL_1)).satisfies(actual -> checkMailEquality(actual, mail));
    }

    @Test
    default void storingMessageWithPerRecipientHeadersShouldAllowMultipleHeadersPerUser() throws Exception {

        MailRepository testee = retrieveRepository();
        Mail mail = createMail(MAIL_1);
        MailAddress recipient1 = new MailAddress("rec1@domain.com");
        mail.addSpecificHeaderForRecipient(PerRecipientHeaders.Header.builder().name("foo").value("bar").build(),
                recipient1);
        mail.addSpecificHeaderForRecipient(PerRecipientHeaders.Header.builder().name("fizz").value("buzz").build(),
                recipient1);
        testee.store(mail);

        assertThat(testee.list()).hasSize(1).containsOnly(MAIL_1);
        assertThat(testee.retrieve(MAIL_1)).satisfies(actual -> checkMailEquality(actual, mail));
    }

    @RepeatedTest(100)
    default void storingAndRemovingMessagesConcurrentlyShouldLeadToConsistentResult() throws Exception {
        MailRepository testee = retrieveRepository();
        int nbKeys = 20;
        ConcurrentHashMap.KeySetView<MailKey, Boolean> expectedResult = ConcurrentHashMap.newKeySet();
        List<Object> locks = IntStream.range(0, nbKeys).boxed().collect(Guavate.toImmutableList());

        Random random = new Random();
        ThrowingRunnable add = () -> {
            int keyIndex = computeKeyIndex(nbKeys, Math.abs(random.nextInt()));
            MailKey key = computeKey(keyIndex);
            synchronized (locks.get(keyIndex)) {
                testee.store(createMail(key));
                expectedResult.add(key);
            }
        };

        ThrowingRunnable remove = () -> {
            int keyIndex = computeKeyIndex(nbKeys, Math.abs(random.nextInt()));
            MailKey key = computeKey(keyIndex);
            synchronized (locks.get(keyIndex)) {
                testee.remove(key);
                expectedResult.remove(key);
            }
        };

        DiscreteDistribution<ThrowingRunnable> distribution = DiscreteDistribution.create(
                ImmutableList.of(new DistributionEntry<>(add, 0.25), new DistributionEntry<>(remove, 0.75)));

        ConcurrentTestRunner.builder().operation((a, b) -> distribution.sample().run()).threadCount(10)
                .operationCount(10).runSuccessfullyWithin(Duration.ofMinutes(1));

        assertThat(testee.list()).containsOnlyElementsOf(expectedResult);
    }

    default MailKey computeKey(int keyIndex) {
        return new MailKey("mail" + keyIndex);
    }

    default int computeKeyIndex(int nbKeys, Integer i) {
        return i % nbKeys;
    }

}