diff --git a/spring-modulith-events/pom.xml b/spring-modulith-events/pom.xml
index 80dda962..d4f46302 100644
--- a/spring-modulith-events/pom.xml
+++ b/spring-modulith-events/pom.xml
@@ -17,6 +17,7 @@
spring-modulith-events-core
spring-modulith-events-jpa
spring-modulith-events-jdbc
+ spring-modulith-events-mongodb
spring-modulith-events-jackson
spring-modulith-events-tests
spring-modulith-events-starter
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java
index e153d8c9..e1ab912c 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java
@@ -94,82 +94,94 @@ class JdbcEventPublicationRepositoryIntegrationTests {
assertThat(repository.findIncompletePublications()).isEmpty();
}
- @Test // GH-3
- void shouldUpdateSingleEventPublication() {
+ @Nested
+ class Update {
- var testEvent1 = new TestEvent("id1");
- var testEvent2 = new TestEvent("id2");
- var serializedEvent1 = "{\"eventId\":\"id1\"}";
- var serializedEvent2 = "{\"eventId\":\"id2\"}";
+ @Test
+ // GH-3
+ void shouldUpdateSingleEventPublication() {
- when(serializer.serialize(testEvent1)).thenReturn(serializedEvent1);
- when(serializer.deserialize(serializedEvent1, TestEvent.class)).thenReturn(testEvent1);
- when(serializer.serialize(testEvent2)).thenReturn(serializedEvent2);
- when(serializer.deserialize(serializedEvent2, TestEvent.class)).thenReturn(testEvent2);
+ var testEvent1 = new TestEvent("id1");
+ var testEvent2 = new TestEvent("id2");
+ var serializedEvent1 = "{\"eventId\":\"id1\"}";
+ var serializedEvent2 = "{\"eventId\":\"id2\"}";
- var publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
- var publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
+ when(serializer.serialize(testEvent1)).thenReturn(serializedEvent1);
+ when(serializer.deserialize(serializedEvent1, TestEvent.class)).thenReturn(testEvent1);
+ when(serializer.serialize(testEvent2)).thenReturn(serializedEvent2);
+ when(serializer.deserialize(serializedEvent2, TestEvent.class)).thenReturn(testEvent2);
- // Store publication
- repository.create(publication1);
- repository.create(publication2);
+ var publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
+ var publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
- // Complete publication
- repository.update(publication2.markCompleted());
+ // Store publication
+ repository.create(publication1);
+ repository.create(publication2);
- assertThat(repository.findIncompletePublications()).hasSize(1)
- .element(0).extracting(EventPublication::getEvent).isEqualTo(testEvent1);
+ // Complete publication
+ repository.update(publication2.markCompleted());
+
+ assertThat(repository.findIncompletePublications()).hasSize(1)
+ .element(0).extracting(EventPublication::getEvent).isEqualTo(testEvent1);
+ }
}
- @Test // GH-3
- void shouldTolerateEmptyResult() {
+ @Nested
+ class FindByEventAndTargetIdentifier {
- var testEvent = new TestEvent("id");
- var serializedEvent = "{\"eventId\":\"id\"}";
+ @Test
+ // GH-3
+ void shouldTolerateEmptyResult() {
- when(serializer.serialize(testEvent)).thenReturn(serializedEvent);
+ var testEvent = new TestEvent("id");
+ var serializedEvent = "{\"eventId\":\"id\"}";
- assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty();
- }
+ when(serializer.serialize(testEvent)).thenReturn(serializedEvent);
- @Test // GH-3
- void shouldReturnTheOldestEvent() throws Exception {
+ assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty();
+ }
- var testEvent = new TestEvent("id");
- var serializedEvent = "{\"eventId\":\"id\"}";
+ @Test
+ // GH-3
+ void shouldReturnTheOldestEvent() throws Exception {
- when(serializer.serialize(testEvent)).thenReturn(serializedEvent);
- when(serializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent);
+ var testEvent = new TestEvent("id");
+ var serializedEvent = "{\"eventId\":\"id\"}";
- var publicationOld = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
- Thread.sleep(10);
- var publicationNew = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
+ when(serializer.serialize(testEvent)).thenReturn(serializedEvent);
+ when(serializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent);
- repository.create(publicationNew);
- repository.create(publicationOld);
+ var publicationOld = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
+ Thread.sleep(10);
+ var publicationNew = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
- var actual = repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
+ repository.create(publicationNew);
+ repository.create(publicationOld);
- assertThat(actual).hasValueSatisfying(it -> {
- assertThat(it.getPublicationDate()).isEqualTo(publicationOld.getPublicationDate());
- });
- }
+ var actual = repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
- @Test // GH-3
- void shouldSilentlyIgnoreNotSerializableEvents() {
+ assertThat(actual).hasValueSatisfying(it -> {
+ assertThat(it.getPublicationDate()).isEqualTo(publicationOld.getPublicationDate());
+ });
+ }
- var testEvent = new TestEvent("id");
- var serializedEvent = "{\"eventId\":\"id\"}";
+ @Test
+ // GH-3
+ void shouldSilentlyIgnoreNotSerializableEvents() {
- when(serializer.serialize(testEvent)).thenReturn(serializedEvent);
- when(serializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent);
+ var testEvent = new TestEvent("id");
+ var serializedEvent = "{\"eventId\":\"id\"}";
- // Store publication
- repository.create(CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER));
+ when(serializer.serialize(testEvent)).thenReturn(serializedEvent);
+ when(serializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent);
- operations.update("UPDATE EVENT_PUBLICATION SET EVENT_TYPE='abc'");
+ // Store publication
+ repository.create(CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER));
- assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty();
+ operations.update("UPDATE EVENT_PUBLICATION SET EVENT_TYPE='abc'");
+
+ assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty();
+ }
}
}
diff --git a/spring-modulith-events/spring-modulith-events-mongodb/pom.xml b/spring-modulith-events/spring-modulith-events-mongodb/pom.xml
new file mode 100644
index 00000000..7f87dd90
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-mongodb/pom.xml
@@ -0,0 +1,51 @@
+
+ 4.0.0
+
+
+ org.springframework.experimental
+ spring-modulith-events
+ 0.1.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Modulith - Events - MongoDB-based repository
+ spring-modulith-events-mongodb
+
+
+ org.springframework.modulith.events.mongodb
+
+
+
+
+
+ ${project.groupId}
+ spring-modulith-events-core
+ ${project.version}
+
+
+
+ org.springframework.data
+ spring-data-mongodb
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+ test
+
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+ 3.4.6
+ test
+
+
+
+
diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublication.java b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublication.java
new file mode 100644
index 00000000..52490b11
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublication.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.modulith.events.mongodb;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceCreator;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.time.Instant;
+
+/**
+ * A MongoDB Document to represent event publications.
+ *
+ * @author Dmitry Belyaev
+ * @author Björn Kieling
+ */
+@Document(collection = "org_springframework_modulith_events")
+@Getter
+@RequiredArgsConstructor
+@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor = @__(@PersistenceCreator))
+class MongoDbEventPublication {
+
+ @Id
+ private String id;
+
+ private final Instant publicationDate;
+ private final String listenerId;
+ private final Object event;
+
+ @Setter(AccessLevel.PACKAGE)
+ private Instant completionDate;
+}
diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java
new file mode 100644
index 00000000..30572160
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.modulith.events.mongodb;
+
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.modulith.events.CompletableEventPublication;
+import org.springframework.modulith.events.EventPublication;
+import org.springframework.modulith.events.EventPublicationRepository;
+import org.springframework.modulith.events.PublicationTargetIdentifier;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import lombok.EqualsAndHashCode;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Repository to store {@link EventPublication}s in a MongoDB.
+ *
+ * @author Björn Kieling
+ * @author Dmitry Belyaev
+ */
+public class MongoDbEventPublicationRepository implements EventPublicationRepository {
+
+ private final MongoTemplate mongoTemplate;
+
+ public MongoDbEventPublicationRepository(MongoTemplate mongoTemplate) {
+ this.mongoTemplate = mongoTemplate;
+ }
+
+ @Override
+ public EventPublication create(EventPublication publication) {
+ mongoTemplate.save(domainToDocument(publication));
+ return publication;
+ }
+
+ @Override
+ public EventPublication update(CompletableEventPublication publication) {
+ findDocumentsByEventAndTargetIdentifier(publication.getEvent(), publication.getTargetIdentifier())
+ .stream()
+ .findFirst()
+ .ifPresent(document -> {
+ document.setCompletionDate(publication.getCompletionDate().orElse(null));
+ mongoTemplate.save(document);
+ });
+ return publication;
+ }
+
+ @Override
+ public List findIncompletePublications() {
+ var query = Query.query(Criteria.where("completionDate").isNull());
+ return mongoTemplate.find(query, MongoDbEventPublication.class).stream() //
+ .map(this::documentToDomain) //
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public Optional findByEventAndTargetIdentifier(
+ Object event, PublicationTargetIdentifier targetIdentifier) {
+
+ var documents = findDocumentsByEventAndTargetIdentifier(event, targetIdentifier);
+ var results = documents
+ .stream() //
+ .map(this::documentToDomain) //
+ .toList();
+
+ // if there are several events with exactly the same payload we return the oldest one first
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ private List findDocumentsByEventAndTargetIdentifier(
+ Object event, PublicationTargetIdentifier targetIdentifier) {
+
+ // we need to enforce writing of the type information
+ var eventAsMongoType = mongoTemplate.getConverter().convertToMongoType(event, TypeInformation.of(Object.class));
+ var query = Query //
+ .query(Criteria //
+ .where("event").is(eventAsMongoType) //
+ .and("listenerId").is(targetIdentifier.getValue())) //
+ .with(Sort.by("publicationDate").ascending());
+ return mongoTemplate.find(query, MongoDbEventPublication.class);
+ }
+
+ private MongoDbEventPublication domainToDocument(EventPublication publication) {
+ return new MongoDbEventPublication( //
+ publication.getPublicationDate(), //
+ publication.getTargetIdentifier().getValue(), //
+ publication.getEvent());
+ }
+
+ private CompletableEventPublication documentToDomain(MongoDbEventPublication document) {
+ return MongoDbEventPublicationAdapter.of(document);
+ }
+
+ @EqualsAndHashCode
+ @RequiredArgsConstructor(staticName = "of")
+ private static class MongoDbEventPublicationAdapter implements CompletableEventPublication {
+
+ private final MongoDbEventPublication publication;
+
+ @Override
+ public Object getEvent() {
+ return publication.getEvent();
+ }
+
+ @Override
+ public PublicationTargetIdentifier getTargetIdentifier() {
+ return PublicationTargetIdentifier.of(publication.getListenerId());
+ }
+
+ @Override
+ public Instant getPublicationDate() {
+ return publication.getPublicationDate();
+ }
+
+ @Override
+ public Optional getCompletionDate() {
+ return Optional.ofNullable(publication.getCompletionDate());
+ }
+
+ @Override
+ public boolean isPublicationCompleted() {
+ return publication.getCompletionDate() != null;
+ }
+
+ @Override
+ public CompletableEventPublication markCompleted() {
+ publication.setCompletionDate(Instant.now());
+ return this;
+ }
+ }
+
+}
diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/package-info.java b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/package-info.java
new file mode 100644
index 00000000..6d371d06
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/package-info.java
@@ -0,0 +1,2 @@
+@org.springframework.lang.NonNullApi
+package org.springframework.modulith.events.mongodb;
diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java
new file mode 100644
index 00000000..32e69bd6
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.modulith.events.mongodb;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
+
+import java.io.IOException;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Optional;
+
+import lombok.Getter;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.modulith.events.CompletableEventPublication;
+import org.springframework.modulith.events.EventPublication;
+import org.springframework.modulith.events.PublicationTargetIdentifier;
+import org.springframework.modulith.testapp.TestApplication;
+import org.springframework.test.context.ContextConfiguration;
+
+import com.mongodb.client.MongoClients;
+
+import de.flapdoodle.embed.mongo.MongodExecutable;
+import de.flapdoodle.embed.mongo.MongodStarter;
+import de.flapdoodle.embed.mongo.config.ImmutableMongodConfig;
+import de.flapdoodle.embed.mongo.config.MongodConfig;
+import de.flapdoodle.embed.mongo.config.Net;
+import de.flapdoodle.embed.mongo.distribution.Version;
+import de.flapdoodle.embed.process.runtime.Network;
+import lombok.EqualsAndHashCode;
+
+/**
+ * @author Björn Kieling
+ * @author Dmitry Belyaev
+ */
+@DataMongoTest
+@ContextConfiguration(classes = TestApplication.class)
+class MongoDbEventPublicationRepositoryTest {
+
+ private static final PublicationTargetIdentifier TARGET_IDENTIFIER = PublicationTargetIdentifier.of("listener");
+ private static final String CONNECTION_STRING = "mongodb://%s:%d";
+
+ private static String ip;
+ private static int port;
+ private static MongodExecutable mongodExecutable;
+
+ private MongoTemplate mongoTemplate;
+
+ private MongoDbEventPublicationRepository repository;
+
+ @BeforeAll
+ static void startupMongoDb() throws IOException {
+
+ // Refer to https://www.baeldung.com/spring-boot-embedded-mongodb
+ ip = "localhost";
+ port = Network.freeServerPort(Network.getLocalHost());
+
+ ImmutableMongodConfig mongodConfig = MongodConfig.builder().version(Version.Main.PRODUCTION)
+ .net(new Net(ip, port, Network.localhostIsIPv6())).build();
+
+ MongodStarter starter = MongodStarter.getDefaultInstance();
+ mongodExecutable = starter.prepare(mongodConfig);
+ mongodExecutable.start();
+ }
+
+ @AfterAll
+ static void shutdownMongoDb() {
+ mongodExecutable.stop();
+ }
+
+ @BeforeEach
+ void setUp() {
+ mongoTemplate = new MongoTemplate(MongoClients.create(String.format(CONNECTION_STRING, ip, port)), "test");
+
+ repository = new MongoDbEventPublicationRepository(mongoTemplate);
+ }
+
+ @AfterEach
+ void tearDown() {
+ mongoTemplate.remove(MongoDbEventPublication.class).all();
+ }
+
+ @Test
+ void shouldPersistAndUpdateEventPublication() {
+
+ TestEvent testEvent = new TestEvent("abc");
+
+ CompletableEventPublication publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
+
+ // Store publication
+ repository.create(publication);
+
+ List eventPublications = repository.findIncompletePublications();
+ assertThat(eventPublications).hasSize(1);
+ assertThat(eventPublications.get(0).getEvent()).isEqualTo(publication.getEvent());
+ assertThat(eventPublications.get(0).getTargetIdentifier()).isEqualTo(publication.getTargetIdentifier());
+
+ assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isPresent();
+
+ // Complete publication
+ repository.update(publication.markCompleted());
+
+ assertThat(repository.findIncompletePublications()).isEmpty();
+ }
+
+ @Test
+ void shouldUpdateSingleEventPublication() {
+ TestEvent testEvent1 = new TestEvent("id1");
+ TestEvent testEvent2 = new TestEvent("id2");
+
+ CompletableEventPublication publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
+ CompletableEventPublication publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
+
+ repository.create(publication1);
+ repository.create(publication2);
+
+ repository.update(publication2.markCompleted());
+
+ List withCompletionDateNull = repository.findIncompletePublications();
+ assertThat(withCompletionDateNull).hasSize(1);
+ assertThat(withCompletionDateNull.get(0).getEvent()).isEqualTo(testEvent1);
+ }
+
+ @Nested
+ class FindByEventAndTargetIdentifier {
+ @Test
+ void shouldFindEventPublicationByEventAndTargetIdentifier() {
+ TestEvent testEvent1 = new TestEvent("abc");
+ TestEvent testEvent2 = new TestEvent("def");
+
+ CompletableEventPublication publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
+ repository.create(publication2);
+
+ CompletableEventPublication publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
+ repository.create(publication1);
+
+ CompletableEventPublication publication3 = CompletableEventPublication.of(
+ testEvent1, PublicationTargetIdentifier.of(TARGET_IDENTIFIER.getValue() + "!"));
+ repository.create(publication3);
+
+ Optional actual = repository.findByEventAndTargetIdentifier(testEvent1, TARGET_IDENTIFIER);
+ assertThat(actual).isPresent();
+ assertThat(actual.get().getEvent()).isEqualTo(testEvent1);
+ assertThat(actual.get().getTargetIdentifier()).isEqualTo(TARGET_IDENTIFIER);
+ }
+
+ @Test
+ void shouldTolerateEmptyResultTest() {
+ TestEvent testEvent = new TestEvent("id");
+
+ Optional actual =
+ repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
+
+ assertThat(actual).isEmpty();
+ }
+
+ @Test
+ void shouldReturnTheOldestEventTest() throws InterruptedException {
+ TestEvent testEvent = new TestEvent("id");
+
+ CompletableEventPublication publicationOld = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
+ Thread.sleep(10);
+ CompletableEventPublication publicationNew = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
+
+ repository.create(publicationNew);
+ repository.create(publicationOld);
+
+ Optional actual =
+ repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
+
+ assertThat(actual).isNotEmpty();
+ assertThat(actual.get().getPublicationDate())
+ .isCloseTo(publicationOld.getPublicationDate(), within(1, ChronoUnit.MILLIS));
+ }
+ }
+
+ @EqualsAndHashCode
+ @Getter
+ private static final class TestEvent {
+ private final String eventId;
+
+ private TestEvent(String eventId) {
+ this.eventId = eventId;
+ }
+ }
+}
diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/testapp/TestApplication.java b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/testapp/TestApplication.java
new file mode 100644
index 00000000..ab85b996
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/testapp/TestApplication.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.modulith.testapp;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Dmitry Belyaev
+ * @author Björn Kieling
+ */
+@SpringBootApplication
+public class TestApplication {
+}