You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2018/05/08 08:42:41 UTC
[2/3] james-project git commit: MAILBOX-331 Implement Quota Threshold
notifier as an EventSourcing project
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: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org