diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/transaction/TransactionPerson.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/transaction/TransactionPerson.java new file mode 100644 index 0000000000000..7d43d5c6bb9f1 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/transaction/TransactionPerson.java @@ -0,0 +1,14 @@ +package io.quarkus.it.mongodb.panache.transaction; + +import org.bson.codecs.pojo.annotations.BsonId; + +import io.quarkus.mongodb.panache.MongoEntity; +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; + +@MongoEntity(database = "transaction-person") +public class TransactionPerson extends PanacheMongoEntityBase { + @BsonId + public Long id; + public String firstname; + public String lastname; +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/transaction/TransactionPersonResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/transaction/TransactionPersonResource.java new file mode 100644 index 0000000000000..4de21c0a970ba --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/transaction/TransactionPersonResource.java @@ -0,0 +1,98 @@ +package io.quarkus.it.mongodb.panache.transaction; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; +import javax.transaction.Transactional; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import com.mongodb.client.MongoClient; + +import io.quarkus.runtime.StartupEvent; + +@Path("/transaction") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class TransactionPersonResource { + @Inject + @Named("cl2") + MongoClient mongoClient; + + void initDb(@Observes StartupEvent startupEvent) { + // in case of transaction, the collection needs to exist prior to using it + if (!mongoClient.getDatabase("transaction-person").listCollectionNames().into(new ArrayList<>()) + .contains("TransactionPerson")) { + mongoClient.getDatabase("transaction-person").createCollection("TransactionPerson"); + } + } + + @GET + @Transactional + public List getPersons() { + return TransactionPerson.listAll(); + } + + @POST + @Transactional + public Response addPerson(TransactionPerson person) { + person.persist(); + return Response.created(URI.create("/transaction/" + person.id.toString())).build(); + } + + @POST + @Path("/exception") + @Transactional + public void addPersonTwice(TransactionPerson person) { + person.persist(); + throw new RuntimeException("You shall not pass"); + } + + @PUT + @Transactional + public Response updatePerson(TransactionPerson person) { + person.update(); + return Response.accepted().build(); + } + + @DELETE + @Path("/{id}") + @Transactional + public void deletePerson(@PathParam("id") String id) { + TransactionPerson person = TransactionPerson.findById(Long.parseLong(id)); + person.delete(); + } + + @GET + @Path("/{id}") + @Transactional + public TransactionPerson getPerson(@PathParam("id") String id) { + return TransactionPerson.findById(Long.parseLong(id)); + } + + @GET + @Path("/count") + @Transactional + public long countAll() { + return TransactionPerson.count(); + } + + @DELETE + @Transactional + public void deleteAll() { + TransactionPerson.deleteAll(); + } + + @POST + @Path("/rename") + @Transactional + public Response rename(@QueryParam("previousName") String previousName, @QueryParam("newName") String newName) { + TransactionPerson.update("lastname", newName).where("lastname", previousName); + return Response.ok().build(); + } +} diff --git a/integration-tests/mongodb-panache/src/main/resources/application.properties b/integration-tests/mongodb-panache/src/main/resources/application.properties index 63810ad397a2a..9f43c244b3c3d 100644 --- a/integration-tests/mongodb-panache/src/main/resources/application.properties +++ b/integration-tests/mongodb-panache/src/main/resources/application.properties @@ -1,9 +1,9 @@ -quarkus.mongodb.connection-string=mongodb://localhost:27018 +quarkus.mongodb.connection-string=mongodb://localhost:27018,localhost:27019 quarkus.mongodb.write-concern.journal=false quarkus.mongodb.database=books # fake a different MongoDB instance -quarkus.mongodb.cl2.connection-string=mongodb://localhost:27018 +quarkus.mongodb.cl2.connection-string=mongodb://localhost:27018,localhost:27019 quarkus.mongodb.cl2.write-concern.journal=false #quarkus.log.category."io.quarkus.mongodb.panache.runtime".level=DEBUG diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongoReplicaSetTestResource.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongoReplicaSetTestResource.java new file mode 100644 index 0000000000000..3e8f4f465fa52 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongoReplicaSetTestResource.java @@ -0,0 +1,160 @@ +package io.quarkus.it.mongodb.panache; + +import static org.awaitility.Awaitility.await; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.bson.Document; +import org.jboss.logging.Logger; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; + +import de.flapdoodle.embed.mongo.MongodExecutable; +import de.flapdoodle.embed.mongo.MongodStarter; +import de.flapdoodle.embed.mongo.config.IMongodConfig; +import de.flapdoodle.embed.mongo.config.MongoCmdOptionsBuilder; +import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.process.runtime.Network; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class MongoReplicaSetTestResource implements QuarkusTestResourceLifecycleManager { + + private static final Logger LOGGER = Logger.getLogger(MongoReplicaSetTestResource.class); + private static List MONGOS = new ArrayList<>(); + + @Override + public Map start() { + try { + List configs = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + int port = 27018 + i; + configs.add(buildMongodConfiguration("localhost", port, true)); + } + configs.forEach(config -> { + MongodExecutable exec = MongodStarter.getDefaultInstance().prepare(config); + MONGOS.add(exec); + try { + exec.start(); + } catch (IOException e) { + LOGGER.error("Unable to start the mongo instance", e); + } + }); + initializeReplicaSet(configs); + return Collections.emptyMap(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void stop() { + MONGOS.forEach(mongod -> { + try { + mongod.stop(); + } catch (Exception e) { + LOGGER.error("Unable to stop MongoDB", e); + } + }); + } + + private static void initializeReplicaSet(final List mongodConfigList) throws UnknownHostException { + final String arbitrerAddress = "mongodb://" + mongodConfigList.get(0).net().getServerAddress().getHostName() + ":" + + mongodConfigList.get(0).net().getPort(); + final MongoClientSettings mo = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(arbitrerAddress)).build(); + + try (MongoClient mongo = MongoClients.create(mo)) { + final MongoDatabase mongoAdminDB = mongo.getDatabase("admin"); + + Document cr = mongoAdminDB.runCommand(new Document("isMaster", 1)); + LOGGER.infof("isMaster: %s", cr); + + // Build replica set configuration settings + final Document rsConfiguration = buildReplicaSetConfiguration(mongodConfigList); + LOGGER.infof("replSetSettings: %s", rsConfiguration); + + // Initialize replica set + cr = mongoAdminDB.runCommand(new Document("replSetInitiate", rsConfiguration)); + LOGGER.infof("replSetInitiate: %s", cr); + + // Check replica set status before to proceed + await() + .pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(1, TimeUnit.MINUTES) + .until(() -> { + Document result = mongoAdminDB.runCommand(new Document("replSetGetStatus", 1)); + LOGGER.infof("replSetGetStatus: %s", result); + return !isReplicaSetStarted(result); + }); + } + } + + private static Document buildReplicaSetConfiguration(final List configList) throws UnknownHostException { + final Document replicaSetSetting = new Document(); + replicaSetSetting.append("_id", "test001"); + + final List members = new ArrayList<>(); + int i = 0; + for (final IMongodConfig mongoConfig : configList) { + members.add(new Document().append("_id", i++).append("host", + mongoConfig.net().getServerAddress().getHostName() + ":" + mongoConfig.net().getPort())); + } + + replicaSetSetting.append("members", members); + return replicaSetSetting; + } + + private static boolean isReplicaSetStarted(final Document setting) { + if (!setting.containsKey("members")) { + return false; + } + + @SuppressWarnings("unchecked") + final List members = setting.get("members", List.class); + for (final Document member : members) { + LOGGER.infof("replica set member %s", member); + final int state = member.getInteger("state"); + LOGGER.infof("state: %s", state); + // 1 - PRIMARY, 2 - SECONDARY, 7 - ARBITER + if (state != 1 && state != 2 && state != 7) { + return false; + } + } + return true; + } + + private static IMongodConfig buildMongodConfiguration(String url, int port, final boolean configureReplicaSet) + throws IOException { + try { + //JDK bug workaround + //https://github.com/quarkusio/quarkus/issues/14424 + //force class init to prevent possible deadlock when done by mongo threads + Class.forName("sun.net.ext.ExtendedSocketOptions", true, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException e) { + } + final MongodConfigBuilder builder = new MongodConfigBuilder() + .version(Version.Main.V4_0) + .net(new Net(url, port, Network.localhostIsIPv6())); + if (configureReplicaSet) { + builder.withLaunchArgument("--replSet", "test001"); + builder.cmdOptions(new MongoCmdOptionsBuilder() + .syncDelay(10) + .useSmallFiles(true) + .useNoJournal(false) + .build()); + } + return builder.build(); + } +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java index e05dccf054da1..cbfff2bacb4dd 100644 --- a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java @@ -21,7 +21,7 @@ import io.quarkus.test.junit.mockito.InjectMock; @QuarkusTest -@QuarkusTestResource(MongoTestResource.class) +@QuarkusTestResource(MongoReplicaSetTestResource.class) public class MongodbPanacheMockingTest { @Test diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java index cf39c6b62baf2..cef77a352c734 100644 --- a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java @@ -31,7 +31,7 @@ import io.restassured.response.Response; @QuarkusTest -@QuarkusTestResource(MongoTestResource.class) +@QuarkusTestResource(MongoReplicaSetTestResource.class) class MongodbPanacheResourceTest { private static final TypeRef> LIST_OF_BOOK_TYPE_REF = new TypeRef>() { }; diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/NativeMongodbPanacheResourceIT.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/NativeReactiveMongodbPanacheResourceIT.java similarity index 56% rename from integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/NativeMongodbPanacheResourceIT.java rename to integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/NativeReactiveMongodbPanacheResourceIT.java index 729ca8be6771e..65f4020287d33 100644 --- a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/NativeMongodbPanacheResourceIT.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/NativeReactiveMongodbPanacheResourceIT.java @@ -3,6 +3,6 @@ import io.quarkus.test.junit.NativeImageTest; @NativeImageTest -class NativeMongodbPanacheResourceIT extends ReactiveMongodbPanacheResourceTest { +class NativeReactiveMongodbPanacheResourceIT extends ReactiveMongodbPanacheResourceTest { } diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/ReactiveMongodbPanacheResourceTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/ReactiveMongodbPanacheResourceTest.java index 52cbe7e9f8387..13d238c4ac288 100644 --- a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/ReactiveMongodbPanacheResourceTest.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/ReactiveMongodbPanacheResourceTest.java @@ -29,7 +29,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.quarkus.it.mongodb.panache.BookDTO; -import io.quarkus.it.mongodb.panache.MongoTestResource; +import io.quarkus.it.mongodb.panache.MongoReplicaSetTestResource; import io.quarkus.it.mongodb.panache.book.BookDetail; import io.quarkus.it.mongodb.panache.person.Person; import io.quarkus.test.common.QuarkusTestResource; @@ -41,7 +41,7 @@ import io.restassured.response.Response; @QuarkusTest -@QuarkusTestResource(MongoTestResource.class) +@QuarkusTestResource(MongoReplicaSetTestResource.class) class ReactiveMongodbPanacheResourceTest { private static final TypeRef> LIST_OF_BOOK_TYPE_REF = new TypeRef>() { }; diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/MongodbPanacheTransactionTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/MongodbPanacheTransactionTest.java new file mode 100644 index 0000000000000..25e2a9b05c5ca --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/MongodbPanacheTransactionTest.java @@ -0,0 +1,143 @@ +package io.quarkus.it.mongodb.panache.transaction; + +import static io.restassured.RestAssured.get; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.quarkus.it.mongodb.panache.MongoReplicaSetTestResource; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.common.mapper.TypeRef; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.parsing.Parser; +import io.restassured.response.Response; + +@QuarkusTest +@QuarkusTestResource(MongoReplicaSetTestResource.class) +class MongodbPanacheTransactionTest { + private static final TypeRef> LIST_OF_PERSON_TYPE_REF = new TypeRef>() { + }; + + @Test + public void testTheEndpoint() { + String endpoint = "/transaction"; + RestAssured.defaultParser = Parser.JSON; + RestAssured.config + .objectMapperConfig(new ObjectMapperConfig().jackson2ObjectMapperFactory((type, s) -> new ObjectMapper() + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS))); + + //delete all + Response response = RestAssured + .given() + .delete(endpoint) + .andReturn(); + Assertions.assertEquals(204, response.statusCode()); + + List list = get(endpoint).as(LIST_OF_PERSON_TYPE_REF); + Assertions.assertEquals(0, list.size()); + + PersonDTO person1 = new PersonDTO(); + person1.id = 1L; + person1.firstname = "John"; + person1.lastname = "Doe"; + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(person1) + .post(endpoint) + .andReturn(); + Assertions.assertEquals(201, response.statusCode()); + + PersonDTO person2 = new PersonDTO(); + person2.id = 2L; + person2.firstname = "Jane"; + person2.lastname = "Doh!"; + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(person2) + .post(endpoint) + .andReturn(); + Assertions.assertEquals(201, response.statusCode()); + + list = get(endpoint).as(LIST_OF_PERSON_TYPE_REF); + Assertions.assertEquals(2, list.size()); + + // This will insert Charles Baudelaire then throws an exception. + // As we are in a transaction Charles Baudelaire will not be saved. + PersonDTO person3 = new PersonDTO(); + person3.id = 3L; + person3.firstname = "Charles"; + person3.lastname = "Baudelaire"; + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(person3) + .post(endpoint + "/exception") + .andReturn(); + Assertions.assertEquals(500, response.statusCode()); + + list = get(endpoint).as(LIST_OF_PERSON_TYPE_REF); + Assertions.assertEquals(2, list.size()); + + //count + Long count = get(endpoint + "/count").as(Long.class); + Assertions.assertEquals(2, count); + + //update a person + person2.lastname = "Doe"; + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(person2) + .put(endpoint) + .andReturn(); + Assertions.assertEquals(202, response.statusCode()); + + //check that the title has been updated + person2 = get(endpoint + "/" + person2.id.toString()).as(PersonDTO.class); + Assertions.assertEquals(2L, person2.id); + Assertions.assertEquals("Doe", person2.lastname); + + //rename the Doe + response = RestAssured + .given() + .queryParam("previousName", "Doe").queryParam("newName", "Dupont") + .header("Content-Type", "application/json") + .when().post(endpoint + "/rename") + .andReturn(); + Assertions.assertEquals(200, response.statusCode()); + + //delete a person + response = RestAssured + .given() + .delete(endpoint + "/" + person2.id.toString()) + .andReturn(); + Assertions.assertEquals(204, response.statusCode()); + + count = get(endpoint + "/count").as(Long.class); + Assertions.assertEquals(1, count); + + //delete all + response = RestAssured + .given() + .delete(endpoint) + .andReturn(); + Assertions.assertEquals(204, response.statusCode()); + + count = get(endpoint + "/count").as(Long.class); + Assertions.assertEquals(0, count); + } + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/NativeMongodbPanacheTransactionIT.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/NativeMongodbPanacheTransactionIT.java new file mode 100644 index 0000000000000..de1cb016fca4c --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/NativeMongodbPanacheTransactionIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.mongodb.panache.transaction; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class NativeMongodbPanacheTransactionIT extends MongodbPanacheTransactionTest { +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/PersonDTO.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/PersonDTO.java new file mode 100644 index 0000000000000..f8ee1093e7438 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/transaction/PersonDTO.java @@ -0,0 +1,7 @@ +package io.quarkus.it.mongodb.panache.transaction; + +public class PersonDTO { + public Long id; + public String firstname; + public String lastname; +}