diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacement.java index 76c102c451..3d7cb09f4d 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacement.java @@ -153,6 +153,26 @@ public static Object updateMany(Object mongoCollection, @ThirdPartyCast(actualTy return handleUpdate("updateManyClientSessionBsonUpdateOptions", mongoCollection, Arrays.asList(clientSession, filter, update, options), filter); } + @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = "findOneAndDeleteBson", usageFilter = UsageFilter.ANY, category = ReplacementCategory.MONGO) + public static Object findOneAndDelete(Object mongoCollection, @ThirdPartyCast(actualType = "org.bson.conversions.Bson") Object filter) { + return handleDelete("findOneAndDeleteBson", mongoCollection, Arrays.asList(filter), filter); + } + + @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = "findOneAndDeleteBsonFindOneAndDeleteOptions", usageFilter = UsageFilter.ANY, category = ReplacementCategory.MONGO) + public static Object findOneAndDelete(Object mongoCollection, @ThirdPartyCast(actualType = "org.bson.conversions.Bson") Object filter, @ThirdPartyCast(actualType = "com.mongodb.client.model.FindOneAndDeleteOptions") Object options) { + return handleDelete("findOneAndDeleteBsonFindOneAndDeleteOptions", mongoCollection, Arrays.asList(filter, options), filter); + } + + @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = "findOneAndDeleteClientSessionBson", usageFilter = UsageFilter.ANY, category = ReplacementCategory.MONGO) + public static Object findOneAndDelete_EM_0(Object mongoCollection, @ThirdPartyCast(actualType = "com.mongodb.client.ClientSession") Object clientSession, @ThirdPartyCast(actualType = "org.bson.conversions.Bson") Object filter) { + return handleDelete("findOneAndDeleteClientSessionBson", mongoCollection, Arrays.asList(clientSession, filter), filter); + } + + @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = "findOneAndDeleteClientSessionBsonFindOneAndDeleteOptions", usageFilter = UsageFilter.ANY, category = ReplacementCategory.MONGO) + public static Object findOneAndDelete(Object mongoCollection, @ThirdPartyCast(actualType = "com.mongodb.client.ClientSession") Object clientSession, @ThirdPartyCast(actualType = "org.bson.conversions.Bson") Object filter, @ThirdPartyCast(actualType = "com.mongodb.client.model.FindOneAndDeleteOptions") Object options) { + return handleDelete("findOneAndDeleteClientSessionBsonFindOneAndDeleteOptions", mongoCollection, Arrays.asList(clientSession, filter, options), filter); + } + /** * Processes the count handling based on the provided parameters. * diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacementTest.java index a376f3de4c..072986707d 100644 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacementTest.java +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MongoCollectionClassReplacementTest.java @@ -1,20 +1,15 @@ package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; import com.mongodb.MongoQueryException; import com.mongodb.client.*; import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.FindOneAndDeleteOptions; import com.mongodb.client.result.DeleteResult; import org.bson.BsonDocument; import org.bson.BsonDocumentWriter; import org.bson.BsonWriter; import org.bson.Document; -import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.EncoderContext; -import org.bson.codecs.configuration.CodecRegistries; -import org.bson.codecs.configuration.CodecRegistry; -import org.bson.codecs.pojo.PojoCodecProvider; import org.evomaster.client.java.instrumentation.AdditionalInfo; import org.evomaster.client.java.instrumentation.MongoFindCommand; import org.evomaster.client.java.instrumentation.object.ClassToSchema; @@ -541,4 +536,89 @@ public void testDeleteManyWithInvalidFilter() { BsonDocument retrievedQuery = assertSingleRecordedCommand(false); assertEquals(-1, retrievedQuery.getDocument("tags").getInt32("$size").getValue()); } + + @Test + public void testFindOneAndDeleteWithFilter() { + final MongoCollection collection = getMongoCollection(); + insertPerson("John Doe", 30); + insertPerson("Jane Doe", 25); + + ExecutionTracer.setExecutingInitMongo(false); + Document deleted = (Document) MongoCollectionClassReplacement.findOneAndDelete(collection, new Document("age", 30)); + + assertNotNull(deleted); + assertEquals("John Doe", deleted.getString("name")); + assertEquals(1, collection.countDocuments()); + BsonDocument retrievedQuery = assertSingleRecordedCommand(true); + assertEquals(30, retrievedQuery.getInt32("age").getValue()); + } + + @Test + public void testFindOneAndDeleteWithFilterNoMatch() { + final MongoCollection collection = getMongoCollection(); + + ExecutionTracer.setExecutingInitMongo(false); + Document deleted = (Document) MongoCollectionClassReplacement.findOneAndDelete(collection, new Document("age", 99)); + + assertNull(deleted); + BsonDocument retrievedQuery = assertSingleRecordedCommand(true); + assertEquals(99, retrievedQuery.getInt32("age").getValue()); + } + + @Test + public void testFindOneAndDeleteWithFilterAndOptions() { + final MongoCollection collection = getMongoCollection(); + insertPerson("John Doe", 30); + + ExecutionTracer.setExecutingInitMongo(false); + Document deleted = (Document) MongoCollectionClassReplacement.findOneAndDelete(collection, new Document("age", 30), new FindOneAndDeleteOptions()); + + assertNotNull(deleted); + BsonDocument retrievedQuery = assertSingleRecordedCommand(true); + assertEquals(30, retrievedQuery.getInt32("age").getValue()); + } + + @Test + public void testFindOneAndDeleteWithClientSessionAndFilter() { + try (ClientSession clientSession = mongoClient.startSession()) { + final MongoCollection collection = getMongoCollection(); + insertPerson("John Doe", 23); + + ExecutionTracer.setExecutingInitMongo(false); + Document deleted = (Document) MongoCollectionClassReplacement.findOneAndDelete_EM_0(collection, clientSession, new Document("age", 23)); + + assertNotNull(deleted); + BsonDocument retrievedQuery = assertSingleRecordedCommand(true); + assertEquals(23, retrievedQuery.getInt32("age").getValue()); + } + } + + @Test + public void testFindOneAndDeleteWithClientSessionFilterAndOptions() { + try (ClientSession clientSession = mongoClient.startSession()) { + final MongoCollection collection = getMongoCollection(); + insertPerson("John Doe", 23); + + ExecutionTracer.setExecutingInitMongo(false); + Document deleted = (Document) MongoCollectionClassReplacement.findOneAndDelete(collection, clientSession, new Document("age", 23), new FindOneAndDeleteOptions()); + + assertNotNull(deleted); + BsonDocument retrievedQuery = assertSingleRecordedCommand(true); + assertEquals(23, retrievedQuery.getInt32("age").getValue()); + } + } + + @Test + public void testFindOneAndDeleteWithInvalidFilter() { + final MongoCollection collection = getMongoCollection(); + insertPerson("John Doe", 30); + + Document invalidFilter = new Document("tags", new Document("$size", -1)); + + ExecutionTracer.setExecutingInitMongo(false); + assertThrows(com.mongodb.MongoException.class, () -> MongoCollectionClassReplacement.findOneAndDelete(collection, invalidFilter)); + + BsonDocument retrievedQuery = assertSingleRecordedCommand(false); + assertEquals(-1, retrievedQuery.getDocument("tags").getInt32("$size").getValue()); + } } diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteApp.java b/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteApp.java new file mode 100644 index 0000000000..6278b7d0ba --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteApp.java @@ -0,0 +1,20 @@ +package com.mongo.findoneanddelete; + +import com.mongo.SwaggerConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@EnableSwagger2 +@SpringBootApplication(exclude = SecurityAutoConfiguration.class) +public class MongoFindOneAndDeleteApp extends SwaggerConfiguration { + public MongoFindOneAndDeleteApp() { + super("mongofindoneanddelete"); + } + + public static void main(String[] args) { + SpringApplication.run(MongoFindOneAndDeleteApp.class, args); + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteController.java b/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteController.java new file mode 100644 index 0000000000..58fb1387f3 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteController.java @@ -0,0 +1,114 @@ +package com.mongo.findoneanddelete; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.Sorts; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.PojoCodecProvider; +import org.bson.conversions.Bson; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Deletes documents directly through the Mongo driver (MongoCollection.findOneAndDelete), + * not through Spring Data, so the filter is only visible at the driver level. + * There is one endpoint for each overload of findOneAndDelete. + */ +@RestController +@RequestMapping(path = "/mongofindoneanddelete") +public class MongoFindOneAndDeleteController { + + /* + Each endpoint uses its own collection, so that a document that + makes one endpoint return 200 cannot accidentally do it for another one. + */ + private static final String FIND_ONE_AND_DELETE_BY_CITY_AND_AGE = "findOneAndDeleteByCityAndAgeCollection"; + private static final String FIND_ONE_AND_DELETE_BY_CITY_AND_AGE_WITH_OPTIONS = "findOneAndDeleteByCityAndAgeWithOptionsCollection"; + private static final String FIND_ONE_AND_DELETE_BY_CITY_AND_AGE_IN_SESSION = "findOneAndDeleteByCityAndAgeInSessionCollection"; + private static final String FIND_ONE_AND_DELETE_BY_CITY_AND_AGE_IN_SESSION_WITH_OPTIONS = "findOneAndDeleteByCityAndAgeInSessionWithOptionsCollection"; + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private MongoClient mongoClient; + + /* + findOneAndDelete returns the deleted document, so (unlike count, delete or update) + the driver needs a codec to decode it into our own class + */ + private static final CodecRegistry POJO_CODEC_REGISTRY = CodecRegistries.fromRegistries( + MongoClientSettings.getDefaultCodecRegistry(), + CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build())); + + private MongoCollection collection(String collectionName) { + return mongoTemplate.getDb() + .getCollection(collectionName, MongoFindOneAndDeleteData.class) + .withCodecRegistry(POJO_CODEC_REGISTRY); + } + + private static Bson filter(String city, int minAge) { + return Filters.and(Filters.eq("city", city), Filters.gte("age", minAge)); + } + + private static FindOneAndDeleteOptions options() { + return new FindOneAndDeleteOptions().sort(Sorts.ascending("age")); + } + + /* + findOneAndDelete returns the deleted document, or null if no document matched the filter + */ + private static ResponseEntity response(MongoFindOneAndDeleteData deleted) { + return ResponseEntity.status(deleted != null ? 200 : 404).build(); + } + + /** + * findOneAndDelete(Bson) + */ + @DeleteMapping("findOneAndDeleteByCityAndAge") + public ResponseEntity findOneAndDeleteByCityAndAge(@RequestParam(name = "city") String city, + @RequestParam(name = "minAge") int minAge) { + return response(collection(FIND_ONE_AND_DELETE_BY_CITY_AND_AGE).findOneAndDelete(filter(city, minAge))); + } + + /** + * findOneAndDelete(Bson, FindOneAndDeleteOptions) + */ + @DeleteMapping("findOneAndDeleteByCityAndAgeWithOptions") + public ResponseEntity findOneAndDeleteByCityAndAgeWithOptions(@RequestParam(name = "city") String city, + @RequestParam(name = "minAge") int minAge) { + return response(collection(FIND_ONE_AND_DELETE_BY_CITY_AND_AGE_WITH_OPTIONS).findOneAndDelete(filter(city, minAge), options())); + } + + /** + * findOneAndDelete(ClientSession, Bson) + */ + @DeleteMapping("findOneAndDeleteByCityAndAgeInSession") + public ResponseEntity findOneAndDeleteByCityAndAgeInSession(@RequestParam(name = "city") String city, + @RequestParam(name = "minAge") int minAge) { + try (ClientSession session = mongoClient.startSession()) { + return response(collection(FIND_ONE_AND_DELETE_BY_CITY_AND_AGE_IN_SESSION).findOneAndDelete(session, filter(city, minAge))); + } + } + + /** + * findOneAndDelete(ClientSession, Bson, FindOneAndDeleteOptions) + */ + @DeleteMapping("findOneAndDeleteByCityAndAgeInSessionWithOptions") + public ResponseEntity findOneAndDeleteByCityAndAgeInSessionWithOptions(@RequestParam(name = "city") String city, + @RequestParam(name = "minAge") int minAge) { + try (ClientSession session = mongoClient.startSession()) { + return response(collection(FIND_ONE_AND_DELETE_BY_CITY_AND_AGE_IN_SESSION_WITH_OPTIONS).findOneAndDelete(session, filter(city, minAge), options())); + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteData.java b/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteData.java new file mode 100644 index 0000000000..b9c8ac6031 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo/src/main/java/com/mongo/findoneanddelete/MongoFindOneAndDeleteData.java @@ -0,0 +1,29 @@ +package com.mongo.findoneanddelete; + +import org.bson.BsonType; +import org.bson.codecs.pojo.annotations.BsonRepresentation; +import org.springframework.data.annotation.Id; + +public class MongoFindOneAndDeleteData { + + /* + The documents in the collection have an ObjectId as _id, while this is a String. + As findOneAndDelete decodes the deleted document with the driver's POJO codec, + it needs to be told how to read it. + */ + @Id + @BsonRepresentation(BsonType.OBJECT_ID) + public String id; + + public String city; + + public int age; + + public MongoFindOneAndDeleteData() { + } + + public MongoFindOneAndDeleteData(String city, int age) { + this.city = city; + this.age = age; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo/src/test/java/com/foo/spring/rest/mongo/findoneanddelete/MongoFindOneAndDeleteAppController.java b/core-tests/e2e-tests/spring/spring-rest-mongo/src/test/java/com/foo/spring/rest/mongo/findoneanddelete/MongoFindOneAndDeleteAppController.java new file mode 100644 index 0000000000..9cbd3777b1 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo/src/test/java/com/foo/spring/rest/mongo/findoneanddelete/MongoFindOneAndDeleteAppController.java @@ -0,0 +1,15 @@ +package com.foo.spring.rest.mongo.findoneanddelete; + +import com.foo.spring.rest.mongo.MongoController; +import com.mongo.findoneanddelete.MongoFindOneAndDeleteApp; + +public class MongoFindOneAndDeleteAppController extends MongoController { + public MongoFindOneAndDeleteAppController() { + super("findoneanddelete", MongoFindOneAndDeleteApp.class); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.mongo.findoneanddelete"; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo/src/test/java/org/evomaster/e2etests/spring/rest/mongo/findoneanddelete/MongoFindOneAndDeleteEMTest.java b/core-tests/e2e-tests/spring/spring-rest-mongo/src/test/java/org/evomaster/e2etests/spring/rest/mongo/findoneanddelete/MongoFindOneAndDeleteEMTest.java new file mode 100644 index 0000000000..b0e14776f6 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo/src/test/java/org/evomaster/e2etests/spring/rest/mongo/findoneanddelete/MongoFindOneAndDeleteEMTest.java @@ -0,0 +1,61 @@ +package org.evomaster.e2etests.spring.rest.mongo.findoneanddelete; + +import com.foo.spring.rest.mongo.findoneanddelete.MongoFindOneAndDeleteAppController; +import org.evomaster.core.EMConfig; +import org.evomaster.core.problem.rest.data.HttpVerb; +import org.evomaster.core.problem.rest.data.RestIndividual; +import org.evomaster.core.search.Solution; +import org.evomaster.e2etests.utils.RestTestBase; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MongoFindOneAndDeleteEMTest extends RestTestBase { + + @BeforeAll + public static void initClass() throws Exception { + EMConfig config = new EMConfig(); + config.setInstrumentMR_MONGO(true); + RestTestBase.initClass(new MongoFindOneAndDeleteAppController()); + } + + @Test + public void testRunEM() throws Throwable { + + runTestHandlingFlakyAndCompilation( + "MongoFindOneAndDeleteEMTest", + "org.foo.spring.rest.mongo.MongoFindOneAndDeleteEMTest", + 1000, + (args) -> { + setOption(args, "heuristicsForMongo", "true"); + setOption(args, "instrumentMR_MONGO", "true"); + setOption(args, "generateMongoData", "true"); + setOption(args, "extractMongoExecutionInfo", "true"); + + Solution solution = initAndRun(args); + + assertTrue(solution.getIndividuals().size() >= 1); + + // one endpoint per overload of MongoCollection.findOneAndDelete. + // A 200 needs a document with the requested city and age in the collection, + // which can only be generated if the findOneAndDelete filter is observed + + // findOneAndDelete(Bson) + assertHasAtLeastOne(solution, HttpVerb.DELETE, 404, "/mongofindoneanddelete/findOneAndDeleteByCityAndAge", null); + assertHasAtLeastOne(solution, HttpVerb.DELETE, 200, "/mongofindoneanddelete/findOneAndDeleteByCityAndAge", null); + + // findOneAndDelete(Bson, FindOneAndDeleteOptions) + assertHasAtLeastOne(solution, HttpVerb.DELETE, 404, "/mongofindoneanddelete/findOneAndDeleteByCityAndAgeWithOptions", null); + assertHasAtLeastOne(solution, HttpVerb.DELETE, 200, "/mongofindoneanddelete/findOneAndDeleteByCityAndAgeWithOptions", null); + + // findOneAndDelete(ClientSession, Bson) + assertHasAtLeastOne(solution, HttpVerb.DELETE, 404, "/mongofindoneanddelete/findOneAndDeleteByCityAndAgeInSession", null); + assertHasAtLeastOne(solution, HttpVerb.DELETE, 200, "/mongofindoneanddelete/findOneAndDeleteByCityAndAgeInSession", null); + + // findOneAndDelete(ClientSession, Bson, FindOneAndDeleteOptions) + assertHasAtLeastOne(solution, HttpVerb.DELETE, 404, "/mongofindoneanddelete/findOneAndDeleteByCityAndAgeInSessionWithOptions", null); + assertHasAtLeastOne(solution, HttpVerb.DELETE, 200, "/mongofindoneanddelete/findOneAndDeleteByCityAndAgeInSessionWithOptions", null); + }); + } +}