http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java new file mode 100644 index 0000000..34b166b --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java @@ -0,0 +1,199 @@ +/**************************************************************** + * 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.mailbox.quota.mailing.subscribers; + +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.io.FileUtils; +import org.apache.james.mailbox.model.Quota; +import org.apache.james.mailbox.quota.QuotaCount; +import org.apache.james.mailbox.quota.QuotaSize; +import org.apache.james.mailbox.quota.model.HistoryEvolution; +import org.apache.james.mailbox.quota.model.QuotaThreshold; +import org.apache.james.mailbox.quota.model.QuotaThresholdChange; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +public class QuotaThresholdNotice { + + public static class Builder { + private Optional<QuotaThreshold> countThreshold; + private Optional<QuotaThreshold> sizeThreshold; + private Quota<QuotaSize> sizeQuota; + private Quota<QuotaCount> countQuota; + + public Builder() { + countThreshold = Optional.empty(); + sizeThreshold = Optional.empty(); + } + + public Builder sizeQuota(Quota<QuotaSize> sizeQuota) { + this.sizeQuota = sizeQuota; + return this; + } + + public Builder countQuota(Quota<QuotaCount> countQuota) { + this.countQuota = countQuota; + return this; + } + + public Builder countThreshold(HistoryEvolution countHistoryEvolution) { + this.countThreshold = Optional.of(countHistoryEvolution) + .filter(this::needsNotification) + .flatMap(HistoryEvolution::getThresholdChange) + .map(QuotaThresholdChange::getQuotaThreshold); + return this; + } + + public Builder sizeThreshold(HistoryEvolution sizeHistoryEvolution) { + this.sizeThreshold = Optional.of(sizeHistoryEvolution) + .filter(this::needsNotification) + .flatMap(HistoryEvolution::getThresholdChange) + .map(QuotaThresholdChange::getQuotaThreshold); + return this; + } + + boolean needsNotification(HistoryEvolution evolution) { + return evolution.getThresholdHistoryChange() == HistoryEvolution.HistoryChangeType.HigherThresholdReached + && evolution.currentThresholdNotRecentlyReached(); + } + + public Optional<QuotaThresholdNotice> build() { + Preconditions.checkNotNull(sizeQuota); + Preconditions.checkNotNull(countQuota); + + if (sizeThreshold.isPresent() || countThreshold.isPresent()) { + return Optional.of( + new QuotaThresholdNotice(countThreshold, sizeThreshold, sizeQuota, countQuota)); + } + return Optional.empty(); + } + } + + public static class MessageBuilder { + public static final String PREAMBLE = "You receive this email because you recently exceeded a threshold related " + + "to the quotas of your email account.\n\n"; + public static final String CONCLUSION = "You need to be aware that actions leading to exceeded quotas will be denied. " + + "This will result in a degraded service.\n" + + "To mitigate this issue you might reach your administrator in order to increase your configured quota. " + + "You might also delete some non important emails."; + + private final StringBuilder stringBuilder; + + + public MessageBuilder() { + this.stringBuilder = new StringBuilder(); + } + + public MessageBuilder appendSizeReport(QuotaThreshold threshold, Quota<QuotaSize> sizeQuota) { + stringBuilder.append(String.format("You currently occupy more than %d %% of the total size allocated to you.\n" + + "You currently occupy %s on a total of %s allocated to you.\n\n", + threshold.getQuotaOccupationRatioAsPercent(), + FileUtils.byteCountToDisplaySize(sizeQuota.getUsed().asLong()), + FileUtils.byteCountToDisplaySize(sizeQuota.getLimit().asLong()))); + return this; + } + + public MessageBuilder appendCountReport(QuotaThreshold threshold, Quota<QuotaCount> countQuota) { + stringBuilder.append(String.format("You currently occupy more than %d %% of the total message count allocated to you.\n" + + "You currently have %d messages on a total of %d allowed for you.\n\n", + threshold.getQuotaOccupationRatioAsPercent(), + countQuota.getUsed().asLong(), + countQuota.getLimit().asLong())); + return this; + } + + public MessageBuilder appendSizeReport(Optional<QuotaThreshold> threshold, Quota<QuotaSize> sizeQuota) { + if (threshold.isPresent()) { + return appendSizeReport(threshold.get(), sizeQuota); + } + return this; + } + + public MessageBuilder appendCountReport(Optional<QuotaThreshold> threshold, Quota<QuotaCount> countQuota) { + if (threshold.isPresent()) { + return appendCountReport(threshold.get(), countQuota); + } + return this; + } + + public MessageBuilder appendPreamble() { + stringBuilder.append(PREAMBLE); + return this; + } + + public MessageBuilder appendConclusion() { + stringBuilder.append(CONCLUSION); + return this; + } + + public String build() { + return stringBuilder.toString(); + } + } + + public static Builder builder() { + return new Builder(); + } + + private final Optional<QuotaThreshold> countThreshold; + private final Optional<QuotaThreshold> sizeThreshold; + private final Quota<QuotaSize> sizeQuota; + private final Quota<QuotaCount> countQuota; + + @VisibleForTesting + QuotaThresholdNotice(Optional<QuotaThreshold> countThreshold, Optional<QuotaThreshold> sizeThreshold, + Quota<QuotaSize> sizeQuota, Quota<QuotaCount> countQuota) { + this.countThreshold = countThreshold; + this.sizeThreshold = sizeThreshold; + this.sizeQuota = sizeQuota; + this.countQuota = countQuota; + } + + public String generateReport() { + return new MessageBuilder() + .appendPreamble() + .appendSizeReport(sizeThreshold, sizeQuota) + .appendCountReport(countThreshold, countQuota) + .appendConclusion() + .build(); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof QuotaThresholdNotice) { + QuotaThresholdNotice that = (QuotaThresholdNotice) o; + + return Objects.equals(this.countThreshold, that.countThreshold) + && Objects.equals(this.sizeThreshold, that.sizeThreshold) + && Objects.equals(this.sizeQuota, that.sizeQuota) + && Objects.equals(this.countQuota, that.countQuota); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(countThreshold, sizeThreshold, sizeQuota, countQuota); + } + +}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java new file mode 100644 index 0000000..e7cdf14 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java @@ -0,0 +1,111 @@ +/**************************************************************** + * 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.mailbox.quota.model; + +import java.util.Objects; +import java.util.Optional; + +public class HistoryEvolution { + + public static HistoryEvolution noChanges() { + return new HistoryEvolution(HistoryChangeType.NoChange, + Optional.empty(), + Optional.empty() + ); + } + + public static HistoryEvolution lowerThresholdReached(QuotaThresholdChange currentThreshold) { + return new HistoryEvolution(HistoryChangeType.LowerThresholdReached, + Optional.empty(), + Optional.of(currentThreshold)); + } + + public static HistoryEvolution higherThresholdReached(QuotaThresholdChange currentThreshold, HighestThresholdRecentness recentness) { + return new HistoryEvolution(HistoryChangeType.HigherThresholdReached, + Optional.of(recentness), + Optional.of(currentThreshold)); + } + + public enum HistoryChangeType { + HigherThresholdReached, + NoChange, + LowerThresholdReached + } + + public enum HighestThresholdRecentness { + AlreadyReachedDuringGracePriod, + NotAlreadyReachedDuringGracePeriod + } + + private final HistoryChangeType thresholdHistoryChange; + private final Optional<HighestThresholdRecentness> recentness; + private final Optional<QuotaThresholdChange> thresholdChange; + + private HistoryEvolution(HistoryChangeType thresholdHistoryChange, Optional<HighestThresholdRecentness> recentness, Optional<QuotaThresholdChange> thresholdChange) { + this.thresholdHistoryChange = thresholdHistoryChange; + this.recentness = recentness; + this.thresholdChange = thresholdChange; + } + + public boolean isChange() { + return thresholdHistoryChange != HistoryChangeType.NoChange; + } + + public boolean currentThresholdNotRecentlyReached() { + return recentness + .map(value -> value == HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod) + .orElse(false); + } + + public Optional<QuotaThresholdChange> getThresholdChange() { + return thresholdChange; + } + + public HistoryChangeType getThresholdHistoryChange() { + return thresholdHistoryChange; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof HistoryEvolution) { + HistoryEvolution that = (HistoryEvolution) o; + + return Objects.equals(this.thresholdHistoryChange, that.thresholdHistoryChange) + && Objects.equals(this.recentness, that.recentness) + && Objects.equals(this.thresholdChange, that.thresholdChange); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(thresholdHistoryChange, recentness, thresholdChange); + } + + + @Override + public String toString() { + return "HistoryEvolution{" + + "thresholdHistoryChange=" + thresholdHistoryChange + + ", recentness=" + recentness + + ", thresholdChange=" + thresholdChange + + '}'; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.java new file mode 100644 index 0000000..dea2676 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.java @@ -0,0 +1,99 @@ +/**************************************************************** + * 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.mailbox.quota.model; + +import java.util.Objects; +import java.util.Optional; + +import org.apache.james.mailbox.model.Quota; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; + +public class QuotaThreshold implements Comparable<QuotaThreshold> { + + public static final QuotaThreshold ZERO = new QuotaThreshold(0.); + + private final double quotaOccupationRatio; + + public QuotaThreshold(double quotaOccupationRatio) { + Preconditions.checkArgument(quotaOccupationRatio >= 0., "Threshold should be contained in [0., 1.] range"); + Preconditions.checkArgument(quotaOccupationRatio <= 1., "Threshold should be contained in [0., 1.] range"); + this.quotaOccupationRatio = quotaOccupationRatio; + } + + public double getQuotaOccupationRatio() { + return quotaOccupationRatio; + } + + public int getQuotaOccupationRatioAsPercent() { + return Double.valueOf(quotaOccupationRatio * 100).intValue(); + } + + public boolean isExceeded(Quota<?> quota) { + if (quota.getLimit().isUnlimited()) { + return false; + } + double used = toDouble(quota.getUsed().asLong()); + double limit = toDouble(quota.getLimit().asLong()); + + double ratio = (used / limit); + + return ratio > quotaOccupationRatio; + } + + public Optional<QuotaThreshold> nonZero() { + if (this.equals(ZERO)) { + return Optional.empty(); + } + return Optional.of(this); + } + + @Override + public int compareTo(QuotaThreshold o) { + return Double.compare(this.quotaOccupationRatio, o.quotaOccupationRatio); + } + + private double toDouble(long aLong) { + return Long.valueOf(aLong).doubleValue(); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof QuotaThreshold) { + QuotaThreshold that = (QuotaThreshold) o; + + return Objects.equals(this.quotaOccupationRatio, that.quotaOccupationRatio); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(quotaOccupationRatio); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("quotaOccupationInPercent", quotaOccupationRatio) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java new file mode 100644 index 0000000..874d1ca --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java @@ -0,0 +1,71 @@ +/**************************************************************** + * 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.mailbox.quota.model; + +import java.time.Instant; +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +public class QuotaThresholdChange { + private final QuotaThreshold quotaThreshold; + private final Instant instant; + + public QuotaThresholdChange(QuotaThreshold quotaThreshold, Instant instant) { + this.quotaThreshold = quotaThreshold; + this.instant = instant; + } + + public boolean isAfter(Instant instant) { + return this.instant.isAfter(instant); + } + + public QuotaThreshold getQuotaThreshold() { + return quotaThreshold; + } + + public Instant getInstant() { + return instant; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof QuotaThresholdChange) { + QuotaThresholdChange that = (QuotaThresholdChange) o; + + return Objects.equals(this.quotaThreshold, that.quotaThreshold) + && Objects.equals(this.instant, that.instant); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(quotaThreshold, instant); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("quotaThreshold", quotaThreshold) + .add("instant", instant) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java new file mode 100644 index 0000000..b80a39f --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java @@ -0,0 +1,109 @@ +/**************************************************************** + * 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.mailbox.quota.model; + +import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.AlreadyReachedDuringGracePriod; +import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.github.steveash.guavate.Guavate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +public class QuotaThresholdHistory { + + private final ImmutableList<QuotaThresholdChange> changes; + + public QuotaThresholdHistory() { + this(ImmutableList.of()); + } + + public QuotaThresholdHistory(QuotaThresholdChange... changes) { + this(Arrays.asList(changes)); + } + + public QuotaThresholdHistory(List<QuotaThresholdChange> changes) { + this.changes = changes.stream() + .sorted(Comparator.comparing(QuotaThresholdChange::getInstant)) + .collect(Guavate.toImmutableList()); + } + + public HistoryEvolution compareWithCurrentThreshold(QuotaThresholdChange thresholdChange, Duration gracePeriod) { + Optional<QuotaThreshold> lastThreshold = Optional.ofNullable(Iterables.getLast(changes, null)) + .map(QuotaThresholdChange::getQuotaThreshold); + + return compareWithCurrentThreshold(thresholdChange, gracePeriod, lastThreshold.orElse(QuotaThreshold.ZERO)); + } + + private HistoryEvolution compareWithCurrentThreshold(QuotaThresholdChange thresholdChange, Duration gracePeriod, QuotaThreshold lastThreshold) { + QuotaThreshold quotaThreshold = thresholdChange.getQuotaThreshold(); + int comparisonResult = quotaThreshold.compareTo(lastThreshold); + + if (comparisonResult < 0) { + return HistoryEvolution.lowerThresholdReached(thresholdChange); + } + if (comparisonResult == 0) { + return HistoryEvolution.noChanges(); + } + return recentlyExceededQuotaThreshold(thresholdChange, gracePeriod) + .map(any -> HistoryEvolution.higherThresholdReached(thresholdChange, AlreadyReachedDuringGracePriod)) + .orElse(HistoryEvolution.higherThresholdReached(thresholdChange, NotAlreadyReachedDuringGracePeriod)); + } + + private Optional<QuotaThresholdChange> recentlyExceededQuotaThreshold(QuotaThresholdChange thresholdChange, Duration gracePeriod) { + return changes.stream() + .filter(change -> change.isAfter(thresholdChange.getInstant().minus(gracePeriod))) + .filter(change -> change.getQuotaThreshold().compareTo(thresholdChange.getQuotaThreshold()) >= 0) + .findFirst(); + } + + public QuotaThresholdHistory combineWith(QuotaThresholdChange change) { + return new QuotaThresholdHistory( + ImmutableList.<QuotaThresholdChange>builder() + .addAll(changes) + .add(change) + .build()); + } + + public ImmutableList<QuotaThresholdChange> getChanges() { + return changes; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof QuotaThresholdHistory) { + QuotaThresholdHistory that = (QuotaThresholdHistory) o; + + return Objects.equals(this.changes, that.changes); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(changes); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java new file mode 100644 index 0000000..7a7cff4 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java @@ -0,0 +1,58 @@ +/**************************************************************** + * 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.mailbox.quota.model; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import org.apache.james.mailbox.model.Quota; + +import com.github.steveash.guavate.Guavate; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; + +public class QuotaThresholds { + private final ImmutableList<QuotaThreshold> quotaThresholds; + + public QuotaThresholds(QuotaThreshold... thresholds) { + this(Arrays.asList(thresholds)); + } + + public QuotaThresholds(List<QuotaThreshold> quotaThresholds) { + this.quotaThresholds = quotaThresholds.stream() + .sorted(Comparator.comparing(QuotaThreshold::getQuotaOccupationRatio).reversed()) + .collect(Guavate.toImmutableList()); + } + + public QuotaThreshold highestExceededThreshold(Quota quota) { + return quotaThresholds.stream() + .filter(quotaLevel -> quotaLevel.isExceeded(quota)) + .findFirst() + .orElse(QuotaThreshold.ZERO); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("quotaThresholds", quotaThresholds) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java new file mode 100644 index 0000000..85fccd1 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java @@ -0,0 +1,46 @@ +/**************************************************************** + * 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.eventsourcing; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +public class DataCollectorSubscriber implements Subscriber { + + private final List<String> data; + + public DataCollectorSubscriber() { + data = new ArrayList<>(); + } + + @Override + public void handle(Event event) { + if (event instanceof TestEvent) { + data.add(((TestEvent) event).getData()); + } + } + + + public List<String> getData() { + return ImmutableList.copyOf(data); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java new file mode 100644 index 0000000..ffdc5f2 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java @@ -0,0 +1,85 @@ +/**************************************************************** + * 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.eventsourcing; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +public class EventIdTest { + + @Test + public void shouldMatchBeanContract() { + EqualsVerifier.forClass(EventStore.History.class) + .allFieldsShouldBeUsed() + .verify(); + } + + @Test + public void firstShouldReturnAConstant() { + assertThat(EventId.first()) + .isEqualTo(EventId.first()); + } + + @Test + public void previousShouldReturnEmptyWhenBeforeFirst() { + assertThat(EventId.first().previous()) + .isEmpty(); + } + + @Test + public void compareToShouldReturnNegativeWhenComparedToNext() { + assertThat(EventId.first()) + .isLessThan(EventId.first().next()); + } + + @Test + public void compareToShouldReturnNegativeWhenComparedToPrevious() { + assertThat(EventId.first().next()) + .isGreaterThan(EventId.first()); + } + + @Test + public void nextShouldAlwaysHaveTheSameIncrement() { + assertThat(EventId.first().next()) + .isEqualTo(EventId.first().next()); + } + + @Test + public void previousShouldRevertNext() { + assertThat(EventId.first().next().previous()) + .contains(EventId.first()); + } + + @Test + public void compareToShouldReturnNegativeWhenComparedToNextWithPreviousCall() { + assertThat(EventId.first().next().previous().get()) + .isLessThan(EventId.first().next()); + } + + @Test + public void compareToShouldReturnNegativeWhenComparedToPreviousWithPreviousCall() { + assertThat(EventId.first().next()) + .isGreaterThan(EventId.first().next().previous().get()); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java new file mode 100644 index 0000000..b2b26a3 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java @@ -0,0 +1,251 @@ +/**************************************************************** + * 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.eventsourcing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyList; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.github.steveash.guavate.Guavate; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + + +interface EventSourcingSystemTest { + + String PAYLOAD_1 = "payload1"; + String PAYLOAD_2 = "payload2"; + TestAggregateId AGGREGATE_ID = TestAggregateId.testId(42); + + class MyCommand implements CommandDispatcher.Command { + private final String payload; + + public MyCommand(String payload) { + this.payload = payload; + } + + public String getPayload() { + return payload; + } + } + + @Test + default void dispatchShouldApplyCommandHandlerThenCallSubscribers(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + + assertThat(subscriber.getData()).containsExactly(PAYLOAD_1); + } + + @Test + default void throwingSubscribersShouldNotAbortSubscriberChain(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of( + events -> { + throw new RuntimeException(); + }, + subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + + assertThat(subscriber.getData()).containsExactly(PAYLOAD_1); + } + + @Test + default void throwingStoreShouldNotLeadToPusblishing() { + EventStore eventStore = mock(EventStore.class); + doThrow(new RuntimeException()).when(eventStore).appendAll(anyList()); + when(eventStore.getEventsOfAggregate(any())).thenReturn(EventStore.History.empty()); + + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of( + events -> { + throw new RuntimeException(); + }, + subscriber), + eventStore); + + assertThatThrownBy(() -> eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1))) + .isInstanceOf(RuntimeException.class); + + assertThat(subscriber.getData()).isEmpty(); + } + + @Test + default void dispatchShouldApplyCommandHandlerThenStoreGeneratedEvents(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + + TestEvent expectedEvent = new TestEvent(EventId.first(), AGGREGATE_ID, PAYLOAD_1); + assertThat(eventStore.getEventsOfAggregate(AGGREGATE_ID).getEvents()) + .containsOnly(expectedEvent); + } + + @Test + default void dispatchShouldCallSubscriberForSubsequentCommands(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_2)); + + assertThat(subscriber.getData()).containsExactly(PAYLOAD_1, PAYLOAD_2); + } + + @Test + default void dispatchShouldStoreEventsForSubsequentCommands(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_2)); + + TestEvent expectedEvent1 = new TestEvent(EventId.first(), AGGREGATE_ID, PAYLOAD_1); + TestEvent expectedEvent2 = new TestEvent(expectedEvent1.eventId().next(), AGGREGATE_ID, PAYLOAD_2); + assertThat(eventStore.getEventsOfAggregate(AGGREGATE_ID).getEvents()) + .containsOnly(expectedEvent1, expectedEvent2); + } + + @Test + default void dispatcherShouldBeAbleToReturnSeveralEvents(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(wordCuttingDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand("This is a test")); + + assertThat(subscriber.getData()).containsExactly("This", "is", "a", "test"); + } + + @Test + default void unknownCommandsShouldBeIgnored(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(wordCuttingDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + assertThatThrownBy(() -> eventSourcingSystem.dispatch(new CommandDispatcher.Command() {})) + .isInstanceOf(CommandDispatcher.UnknownCommandException.class); + } + + @Test + default void constructorShouldThrowWhenSeveralHandlersForTheSameCommand(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + + assertThatThrownBy(() -> + new EventSourcingSystem( + ImmutableSet.of(wordCuttingDispatcher(eventStore), + simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore)) + .isInstanceOf(IllegalArgumentException.class); + } + + default CommandDispatcher.CommandHandler<MyCommand> simpleDispatcher(EventStore eventStore) { + return new CommandDispatcher.CommandHandler<MyCommand>() { + @Override + public Class<MyCommand> handledClass() { + return MyCommand.class; + } + + @Override + public List<? extends Event> handle(MyCommand myCommand) { + EventStore.History history = eventStore.getEventsOfAggregate(AGGREGATE_ID); + + return ImmutableList.of(new TestEvent( + history.getNextEventId(), + AGGREGATE_ID, + myCommand.getPayload())); + } + }; + } + + default CommandDispatcher.CommandHandler<MyCommand> wordCuttingDispatcher(EventStore eventStore) { + return new CommandDispatcher.CommandHandler<MyCommand>() { + @Override + public Class<MyCommand> handledClass() { + return MyCommand.class; + } + + @Override + public List<? extends Event> handle(MyCommand myCommand) { + EventStore.History history = eventStore.getEventsOfAggregate(AGGREGATE_ID); + + EventIdIncrementer eventIdIncrementer = new EventIdIncrementer(history.getNextEventId()); + + return Splitter.on(" ") + .splitToList(myCommand.getPayload()) + .stream() + .map(word -> new TestEvent( + eventIdIncrementer.next(), + AGGREGATE_ID, + word)) + .collect(Guavate.toImmutableList()); + } + }; + } + + class EventIdIncrementer { + private EventId currentEventId; + + public EventIdIncrementer(EventId base) { + this.currentEventId = base; + } + + public EventId next() { + currentEventId = currentEventId.next(); + return currentEventId; + } + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java new file mode 100644 index 0000000..c847f81 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java @@ -0,0 +1,75 @@ +package org.apache.james.eventsourcing; + +import static org.apache.james.eventsourcing.TestAggregateId.testId; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableList; + +import nl.jqno.equalsverifier.EqualsVerifier; + +interface EventStoreTest { + + TestAggregateId AGGREGATE_1 = testId(1); + TestAggregateId AGGREGATE_2 = testId(2); + + @Test + default void historyShouldMatchBeanContract() { + EqualsVerifier.forClass(EventStore.History.class) + .allFieldsShouldBeUsed() + .verify(); + } + + @Test + default void getEventsOfAggregateShouldThrowOnNullAggregateId(EventStore testee) { + assertThatThrownBy(() -> testee.getEventsOfAggregate(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + default void appendShouldThrowWhenEventFromSeveralAggregates(EventStore testee) { + TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + TestEvent event2 = new TestEvent(event1.eventId().next(), AGGREGATE_2, "second"); + assertThatThrownBy(() -> testee.appendAll(event1, event2)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + default void appendShouldDoNothingOnEmptyEventList(EventStore testee) { + assertThatCode(testee::appendAll).doesNotThrowAnyException(); + } + + @Test + default void appendShouldThrowWhenTryingToRewriteHistory(EventStore testee) { + TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + testee.append(event1); + TestEvent event2 = new TestEvent(EventId.first(), AGGREGATE_1, "second"); + assertThatThrownBy(() -> testee.append(event2)).isInstanceOf(EventStore.EventStoreFailedException.class); + } + + @Test + default void getEventsOfAggregateShouldReturnEmptyHistoryWhenUnknown(EventStore testee) { + assertThat(testee.getEventsOfAggregate(AGGREGATE_1)).isEqualTo(EventStore.History.empty()); + } + + @Test + default void getEventsOfAggregateShouldReturnAppendedEvent(EventStore testee) { + TestEvent event = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + testee.append(event); + assertThat(testee.getEventsOfAggregate(AGGREGATE_1)) + .isEqualTo(EventStore.History.of(ImmutableList.of(event))); + } + + @Test + default void getEventsOfAggregateShouldReturnAppendedEvents(EventStore testee) { + TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + TestEvent event2 = new TestEvent(event1.eventId().next(), AGGREGATE_1, "second"); + testee.append(event1); + testee.append(event2); + assertThat(testee.getEventsOfAggregate(AGGREGATE_1)) + .isEqualTo(EventStore.History.of(ImmutableList.of(event1, event2))); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java new file mode 100644 index 0000000..9419087 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java @@ -0,0 +1,87 @@ +/**************************************************************** + * 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.eventsourcing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +public class HistoryTest { + + @Test + public void shouldMatchBeanContract() { + EqualsVerifier.forClass(EventStore.History.class) + .verify(); + } + + @Test + public void emptyShouldGenerateAnEmptyHistory() { + assertThat(EventStore.History.empty()) + .isEqualTo(EventStore.History.of()); + } + + @Test + public void getVersionShouldReturnEmptyWhenEmpty() { + assertThat(EventStore.History.empty() + .getVersion()) + .isEmpty(); + } + + @Test + public void getVersionShouldReturnSingleEventIdWhenSingleEvent() { + assertThat(EventStore.History + .of(new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any")) + .getVersion()) + .contains(EventId.first()); + } + + @Test + public void getVersionShouldReturnHighestEventId() { + TestEvent event1 = new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any"); + TestEvent event2 = new TestEvent(event1.eventId().next(), + TestAggregateId.testId(42), + "any"); + + assertThat(EventStore.History.of(event1, event2) + .getVersion()) + .contains(event2.eventId()); + } + + @Test + public void duplicateHistoryShouldThrow() { + TestEvent event1 = new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any"); + TestEvent event2 = new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any"); + + assertThatThrownBy(() -> EventStore.History.of(event1, event2)) + .isInstanceOf(EventStore.EventStoreFailedException.class); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java new file mode 100644 index 0000000..1d7c8af --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java @@ -0,0 +1,64 @@ +/**************************************************************** + * 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.eventsourcing; + +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +public class TestAggregateId implements AggregateId { + + public static TestAggregateId testId(int id) { + return new TestAggregateId(id); + } + + private final int id; + + private TestAggregateId(int id) { + this.id = id; + } + + @Override + public String asAggregateKey() { + return "TestAggregateId-" + id; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof TestAggregateId) { + TestAggregateId that = (TestAggregateId) o; + + return Objects.equals(this.id, that.id); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java new file mode 100644 index 0000000..c46f804 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java @@ -0,0 +1,82 @@ +/**************************************************************** + * 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.eventsourcing; + +import java.util.Comparator; +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +public class TestEvent implements Event { + private final EventId id; + private final TestAggregateId aggregateId; + private final String data; + + public TestEvent(EventId id, TestAggregateId aggregateId, String data) { + this.id = id; + this.aggregateId = aggregateId; + this.data = data; + } + + @Override + public EventId eventId() { + return id; + } + + @Override + public TestAggregateId getAggregateId() { + return aggregateId; + } + + public String getData() { + return data; + } + + @Override + public int compareTo(Event o) { + return Comparator.<EventId>naturalOrder().compare(id, o.eventId()); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof TestEvent) { + TestEvent testEvent = (TestEvent) o; + + return Objects.equals(this.id, testEvent.id) + && Objects.equals(this.aggregateId, testEvent.aggregateId) + && Objects.equals(this.data, testEvent.data); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(id, aggregateId, data); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("aggregateId", aggregateId) + .add("data", data) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java new file mode 100644 index 0000000..45aef96 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java @@ -0,0 +1,47 @@ +/**************************************************************** + * 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.mailbox.quota.mailing.aggregates; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.james.core.User; +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +public class UserQuotaThresholdsTest { + + public static final User BOB = User.fromUsername("bob@domain"); + + @Test + public void aggregateShouldMatchBeanContract() { + EqualsVerifier.forClass(UserQuotaThresholds.Id.class) + .allFieldsShouldBeUsed() + .verify(); + } + + @Test + public void asAggregationKeyShouldConvertAggregateToAStringRepresentation() { + assertThat(UserQuotaThresholds.Id.from(BOB) + .asAggregateKey()) + .isEqualTo("QuotaThreasholdEvents-bob@domain"); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java new file mode 100644 index 0000000..9a287c2 --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java @@ -0,0 +1,33 @@ +/**************************************************************** + * 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.mailbox.quota.mailing.commands; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +public class DetectThresholdCrossingTest { + + @Test + public void aggregateShouldMatchBeanContract() { + EqualsVerifier.forClass(DetectThresholdCrossing.class) + .verify(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java new file mode 100644 index 0000000..cfae4fc --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java @@ -0,0 +1,58 @@ +/**************************************************************** + * 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.mailbox.quota.mailing.listeners; + +import org.apache.james.eventsourcing.EventSourcingSystem; +import org.apache.james.eventsourcing.EventStore; +import org.apache.james.mailbox.Event; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.mock.MockMailboxSession; +import org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration; +import org.apache.james.mailbox.quota.mailing.commands.DetectThresholdCrossingHandler; +import org.apache.james.mailbox.quota.mailing.subscribers.QuotaThresholdMailer; +import org.apache.james.mailbox.store.event.DefaultDelegatingMailboxListener; +import org.apache.james.user.memory.MemoryUsersRepository; +import org.apache.mailet.MailetContext; + +import com.google.common.collect.ImmutableSet; + +public class QuotaThresholdListenersTestSystem { + + private final DefaultDelegatingMailboxListener delegatingListener; + + public QuotaThresholdListenersTestSystem(MailetContext mailetContext, EventStore eventStore, QuotaMailingListenerConfiguration configuration) throws MailboxException { + delegatingListener = new DefaultDelegatingMailboxListener(); + + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(new DetectThresholdCrossingHandler(eventStore, configuration)), + ImmutableSet.of(new QuotaThresholdMailer(mailetContext, MemoryUsersRepository.withVirtualHosting())), + eventStore); + + QuotaThresholdCrossingListener thresholdCrossingListener = + new QuotaThresholdCrossingListener(eventSourcingSystem); + + MockMailboxSession mailboxSession = new MockMailboxSession("system"); + delegatingListener.addGlobalListener(thresholdCrossingListener, mailboxSession); + } + + public void event(Event event) { + delegatingListener.event(event); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java new file mode 100644 index 0000000..54854cc --- /dev/null +++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java @@ -0,0 +1,217 @@ +/**************************************************************** + * 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.mailbox.quota.mailing.listeners; + +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.BOB_SESSION; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.DEFAULT_CONFIGURATION; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.GRACE_PERIOD; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.NOW; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.ONE_HOUR_AGO; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.QUOTAROOT; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.SIX_DAYS_AGO; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.SIX_HOURS_AGO; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.TWELVE_DAYS_AGO; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.TWELVE_HOURS_AGO; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.TWO_DAYS_AGO; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._50; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._80; +import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.mailetContext; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.TimeUnit; + +import org.apache.james.eventsourcing.EventStore; +import org.apache.james.mailbox.MailboxListener.QuotaUsageUpdatedEvent; +import org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration; +import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Counts; +import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Sizes; +import org.apache.james.mailbox.quota.model.QuotaThresholds; +import org.apache.james.util.concurrency.ConcurrentTestRunner; +import org.apache.mailet.base.test.FakeMailContext; +import org.junit.jupiter.api.Test; + +public interface QuotaThresholdMailingIntegrationTest { + + @Test + default void shouldNotSendMailWhenUnderAllThresholds(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).isEmpty(); + } + + @Test + default void shouldNotSendMailWhenNoThresholdUpdate(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, ONE_HOUR_AGO)); + mailetContext.resetSentMails(); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).isEmpty(); + } + + @Test + default void shouldNotSendMailWhenThresholdOverPassedRecently(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, TWELVE_HOURS_AGO)); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_HOURS_AGO)); + mailetContext.resetSentMails(); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).isEmpty(); + } + + @Test + default void shouldSendMailWhenThresholdOverPassed(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).hasSize(1); + } + + @Test + default void shouldNotSendDuplicates(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, ONE_HOUR_AGO)); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).hasSize(1); + } + + @Test + default void shouldNotifySeparatelyCountAndSize(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, ONE_HOUR_AGO)); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._60_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).hasSize(2); + } + + @Test + default void shouldGroupSizeAndCountNotificationsWhenTriggeredByASingleEvent(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._55_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).hasSize(1); + } + + @Test + default void shouldSendMailWhenThresholdOverPassedOverGracePeriod(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, TWELVE_DAYS_AGO)); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_DAYS_AGO)); + mailetContext.resetSentMails(); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).hasSize(1); + } + + @Test + default void shouldNotSendMailWhenNoThresholdUpdateForCount(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._32_PERCENT, Sizes._55_PERCENT, TWO_DAYS_AGO)); + mailetContext.resetSentMails(); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._60_PERCENT, TWO_DAYS_AGO)); + + assertThat(mailetContext.getSentMails()).isEmpty(); + } + + @Test + default void shouldNotSendMailWhenThresholdOverPassedRecentlyForCount(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, TWELVE_HOURS_AGO)); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_HOURS_AGO)); + mailetContext.resetSentMails(); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).isEmpty(); + } + + @Test + default void shouldSendMailWhenThresholdOverPassedForCount(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, TWELVE_HOURS_AGO)); + + assertThat(mailetContext.getSentMails()).hasSize(1); + } + + @Test + default void shouldSendMailWhenThresholdOverPassedOverGracePeriodForCount(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, TWELVE_DAYS_AGO)); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_DAYS_AGO)); + mailetContext.resetSentMails(); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()).hasSize(1); + } + + @Test + default void shouldSendOneNoticePerThreshold(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, + new QuotaMailingListenerConfiguration(new QuotaThresholds(_50, _80), GRACE_PERIOD)); + + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, NOW)); + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._85_PERCENT, Sizes._42_PERCENT, NOW)); + + assertThat(mailetContext.getSentMails()) + .hasSize(2); + } + + @Test + default void shouldSendOneMailUponConcurrentEvents(EventStore store) throws Exception { + FakeMailContext mailetContext = mailetContext(); + QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, new QuotaMailingListenerConfiguration(new QuotaThresholds(_50, _80), GRACE_PERIOD)); + + new ConcurrentTestRunner(10, 1, (threadNb, step) -> + testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW))) + .run() + .awaitTermination(1, TimeUnit.MINUTES); + + assertThat(mailetContext.getSentMails()) + .hasSize(1); + } + +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
