diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/mongo/BsonDocumentConverter.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/mongo/BsonDocumentConverter.java index 8574f83724..e8b3b3a6e8 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/mongo/BsonDocumentConverter.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/mongo/BsonDocumentConverter.java @@ -18,6 +18,10 @@ public class BsonDocumentConverter { public static final String DECODE = "decode"; public static final String BUILDER = "builder"; public static final String BUILD = "build"; + private static final String ORG_BSON_CODECS_CONFIGURATION_CODEC_REGISTRY = "org.bson.codecs.configuration.CodecRegistry"; + private static final String COM_MONGODB_MONGO_CLIENT_SETTINGS = "com.mongodb.MongoClientSettings"; + private static final String COM_MONGODB_MONGO_CLIENT = "com.mongodb.MongoClient"; + private static final String GET_DEFAULT_CODEC_REGISTRY = "getDefaultCodecRegistry"; /** * Checks if the given object is an instance of a org.bson.BsonDocument. @@ -62,14 +66,41 @@ public static boolean isBsonDocument(Object value) { if (bsonDocumentClass.isInstance(bson)) { return bson; } - Object bsonDocument = bsonClass.getMethod(TO_BSON_DOCUMENT).invoke(bson); - return bsonDocument; + return invokeToBsonDocument(bsonClass, bsonDocumentClass, bson); } catch (Exception | LinkageError e) { SimpleLogger.uniqueWarn("Failed to convert Mongo query from Bson: " + e); return null; } } + /** + * Calls the no-arg Bson.toBsonDocument(), which only exists since driver 4.2. + * For older drivers, falls back to toBsonDocument(Class, CodecRegistry) with the default registry. + */ + private static Object invokeToBsonDocument(Class bsonClass, Class bsonDocumentClass, Object bson) throws Exception { + try { + return bsonClass.getMethod(TO_BSON_DOCUMENT).invoke(bson); + } catch (NoSuchMethodException e) { + // driver < 4.2 + Class codecRegistryClass = loadClass(ORG_BSON_CODECS_CONFIGURATION_CODEC_REGISTRY, bson); + Object registry = getDefaultCodecRegistry(bson); + return bsonClass.getMethod(TO_BSON_DOCUMENT, Class.class, codecRegistryClass) + .invoke(bson, bsonDocumentClass, registry); + } + } + + private static Object getDefaultCodecRegistry(Object instance) throws Exception { + try { + // driver >= 3.7 + return loadClass(COM_MONGODB_MONGO_CLIENT_SETTINGS, instance) + .getMethod(GET_DEFAULT_CODEC_REGISTRY).invoke(null); + } catch (ClassNotFoundException | NoSuchMethodException e) { + // legacy driver (eg, 3.2) + return loadClass(COM_MONGODB_MONGO_CLIENT, instance) + .getMethod(GET_DEFAULT_CODEC_REGISTRY).invoke(null); + } + } + /** * Converts an object of type org.bson.BsonDocument to a corresponding org.bson.Document object. * If the input object is null, the method returns null. It also handles cases where the input diff --git a/core-tests/e2e-tests/spring/pom.xml b/core-tests/e2e-tests/spring/pom.xml index 909a91e9a2..36b633a94a 100644 --- a/core-tests/e2e-tests/spring/pom.xml +++ b/core-tests/e2e-tests/spring/pom.xml @@ -23,6 +23,7 @@ spring-rest-h2-z3solver spring-web spring-rest-mongo + spring-rest-mongo-legacy spring-rest-bb spring-graphql-bb spring-rest-multidb diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/pom.xml b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/pom.xml new file mode 100644 index 0000000000..23b0ac146f --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + + evomaster-e2e-tests-spring + org.evomaster + 6.2.1-SNAPSHOT + + + evomaster-e2e-tests-spring-rest-mongo-legacy + jar + + + ${skipMongo} + + 3.8.2 + + + + + org.evomaster + evomaster-e2e-tests-utils + test-jar + + + org.evomaster + evomaster-client-java-controller + + + org.evomaster + evomaster-core + test + + + org.evomaster + evomaster-client-java-instrumentation + test-jar + + + org.junit.jupiter + junit-jupiter-api + test + + + org.springframework.boot + spring-boot-starter-web + + + + org.mongodb + mongo-java-driver + ${mongo.legacy.driver.version} + + + + javax.validation + validation-api + 2.0.1.Final + + + org.testcontainers + testcontainers + + + io.springfox + springfox-swagger2 + + + io.swagger + * + + + + + io.rest-assured + rest-assured + + + + + + + + kotlin-maven-plugin + org.jetbrains.kotlin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + false + + + + + + diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/SwaggerConfiguration.java b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/SwaggerConfiguration.java new file mode 100644 index 0000000000..13896abe05 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/SwaggerConfiguration.java @@ -0,0 +1,37 @@ +package com.mongolegacy; + +import org.springframework.context.annotation.Bean; +import org.springframework.web.context.request.WebRequest; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +import static springfox.documentation.builders.PathSelectors.regex; + +public class SwaggerConfiguration { + + String path; + + public SwaggerConfiguration(String path) { + this.path = path; + } + + @Bean + public Docket docketApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .paths(regex("/" + path + "/.*")) + .build() + .ignoredParameterTypes(WebRequest.class); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("API") + .description("Some description") + .version("1.0") + .build(); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyMongoPersonsApp.java b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyMongoPersonsApp.java new file mode 100644 index 0000000000..354b841da9 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyMongoPersonsApp.java @@ -0,0 +1,34 @@ +package com.mongolegacy.persons; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongolegacy.SwaggerConfiguration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.context.annotation.Bean; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * SUT using directly the legacy MongoDB driver 3.8 (no Spring Data). + * Spring Boot auto-configuration for Mongo is excluded, as it targets 4.x drivers. + */ +@EnableSwagger2 +@SpringBootApplication(exclude = MongoAutoConfiguration.class) +public class LegacyMongoPersonsApp extends SwaggerConfiguration { + + public LegacyMongoPersonsApp() { + super("legacymongo"); + } + + @Bean(destroyMethod = "close") + public MongoClient mongoClient(@Value("${spring.data.mongodb.host}") String host, + @Value("${spring.data.mongodb.port}") int port) { + return MongoClients.create("mongodb://" + host + ":" + port); + } + + public static void main(String[] args) { + SpringApplication.run(LegacyMongoPersonsApp.class, args); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyMongoPersonsRest.java b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyMongoPersonsRest.java new file mode 100644 index 0000000000..5c3a1237ed --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyMongoPersonsRest.java @@ -0,0 +1,47 @@ +package com.mongolegacy.persons; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import org.bson.Document; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(path = "/legacymongo") +public class LegacyMongoPersonsRest { + + @Autowired + private MongoClient mongoClient; + + @Value("${spring.data.mongodb.database}") + private String databaseName; + + private MongoCollection persons() { + return mongoClient.getDatabase(databaseName).getCollection("persons"); + } + + @PostMapping("/persons") + public ResponseEntity create(@RequestBody LegacyPersonDto dto) { + if (dto == null || dto.name == null || dto.age == null) { + return ResponseEntity.status(400).build(); + } + persons().insertOne(new Document("name", dto.name).append("age", dto.age)); + return ResponseEntity.status(201).build(); + } + + @GetMapping("/persons") + public ResponseEntity find(@RequestParam String name, @RequestParam int age) { + /* + Filters do not return a BsonDocument. With driver 3.x, converting them + requires Bson.toBsonDocument(Class, CodecRegistry), as the no-arg version only exists since 4.2 + */ + Document found = persons() + .find(Filters.and(Filters.eq("name", name), Filters.gte("age", age))) + .first(); + int status = found != null ? 200 : 404; + return ResponseEntity.status(status).build(); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyPersonDto.java b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyPersonDto.java new file mode 100644 index 0000000000..ab7c39ab2c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/main/java/com/mongolegacy/persons/LegacyPersonDto.java @@ -0,0 +1,8 @@ +package com.mongolegacy.persons; + +public class LegacyPersonDto { + + public String name; + + public Integer age; +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/com/foo/spring/rest/mongolegacy/LegacyMongoPersonsAppController.java b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/com/foo/spring/rest/mongolegacy/LegacyMongoPersonsAppController.java new file mode 100644 index 0000000000..c15be87a69 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/com/foo/spring/rest/mongolegacy/LegacyMongoPersonsAppController.java @@ -0,0 +1,121 @@ +package com.foo.spring.rest.mongolegacy; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongolegacy.persons.LegacyMongoPersonsApp; +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbSpecification; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.testcontainers.containers.GenericContainer; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class LegacyMongoPersonsAppController extends EmbeddedSutController { + + private static final int MONGODB_PORT = 27017; + + private static final String DATABASE_NAME = "legacymongo"; + + private MongoClient mongoClient; + + /* + MongoDB 4.4 is a server version supported by the legacy 3.8 driver wire protocol, + and it has images for both amd64 and arm64 + */ + private final GenericContainer mongodb = new GenericContainer<>("mongo:4.4") + .withTmpFs(Collections.singletonMap("/data/db", "rw")) + .withExposedPorts(MONGODB_PORT); + + private ConfigurableApplicationContext ctx; + + public LegacyMongoPersonsAppController() { + super.setControllerPort(0); + } + + @Override + public String startSut() { + mongodb.start(); + + String host = mongodb.getHost(); + int port = mongodb.getMappedPort(MONGODB_PORT); + + mongoClient = MongoClients.create("mongodb://" + host + ":" + port + "/" + DATABASE_NAME); + + SpringApplicationBuilder app = new SpringApplicationBuilder(LegacyMongoPersonsApp.class); + + app.properties( + "--server.port=0", + "spring.data.mongodb.host=" + host, + "spring.data.mongodb.port=" + port, + "spring.data.mongodb.database=" + DATABASE_NAME + ); + + ctx = app.run(); + + return "http://localhost:" + getSutPort(); + } + + @Override + public void stopSut() { + ctx.stop(); + ctx.close(); + mongoClient.close(); + mongodb.stop(); + } + + @Override + public void resetStateOfSUT() { + mongoClient.getDatabase(DATABASE_NAME).drop(); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.mongolegacy.persons"; + } + + @Override + public List getDbSpecifications() { + return null; + } + + @Override + public boolean isSutRunning() { + return ctx != null && ctx.isRunning(); + } + + @Override + public List getInfoForAuthentication() { + return null; + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + getSutPort() + "/v2/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return null; + } + + protected int getSutPort() { + return (Integer) ((Map) ctx.getEnvironment() + .getPropertySources().get("server.ports").getSource()) + .get("local.server.port"); + } + + @Override + public MongoClient getMongoConnection() { + return mongoClient; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/org/evomaster/e2etests/spring/rest/mongolegacy/persons/LegacyMongoBsonDocumentConverterTest.java b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/org/evomaster/e2etests/spring/rest/mongolegacy/persons/LegacyMongoBsonDocumentConverterTest.java new file mode 100644 index 0000000000..cac40ba235 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/org/evomaster/e2etests/spring/rest/mongolegacy/persons/LegacyMongoBsonDocumentConverterTest.java @@ -0,0 +1,33 @@ +package org.evomaster.e2etests.spring.rest.mongolegacy.persons; + +import com.mongodb.client.model.Filters; +import org.bson.BsonDocument; +import org.bson.conversions.Bson; +import org.evomaster.client.java.instrumentation.mongo.BsonDocumentConverter; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Checks that the conversion of Bson queries works with the legacy MongoDB driver 3.8 + */ +public class LegacyMongoBsonDocumentConverterTest { + + @Test + public void testLegacyDriverIsOnClasspath() { + // the no-arg Bson.toBsonDocument() was introduced in driver 4.2 + assertThrows(NoSuchMethodException.class, () -> Bson.class.getMethod("toBsonDocument")); + } + + @Test + public void testToBsonDocumentFromFilters() { + Bson filter = Filters.and(Filters.eq("name", "foo"), Filters.gte("age", 42)); + Object converted = BsonDocumentConverter.toBsonDocument(filter); + + assertNotNull(converted); + assertTrue(converted instanceof BsonDocument); + assertEquals( + BsonDocument.parse("{\"name\": \"foo\", \"age\": {\"$gte\": 42}}"), + converted); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/org/evomaster/e2etests/spring/rest/mongolegacy/persons/LegacyMongoPersonsEMTest.java b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/org/evomaster/e2etests/spring/rest/mongolegacy/persons/LegacyMongoPersonsEMTest.java new file mode 100644 index 0000000000..791b26bc99 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mongo-legacy/src/test/java/org/evomaster/e2etests/spring/rest/mongolegacy/persons/LegacyMongoPersonsEMTest.java @@ -0,0 +1,46 @@ +package org.evomaster.e2etests.spring.rest.mongolegacy.persons; + +import com.foo.spring.rest.mongolegacy.LegacyMongoPersonsAppController; +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.assertFalse; + +/** + * Same as other Mongo E2E tests, but with the SUT using the legacy MongoDB driver 3.8 + */ +public class LegacyMongoPersonsEMTest extends RestTestBase { + + @BeforeAll + public static void initClass() throws Exception { + EMConfig config = new EMConfig(); + config.setInstrumentMR_MONGO(true); + RestTestBase.initClass(new LegacyMongoPersonsAppController(), config); + } + + @Test + public void testRunEM() throws Throwable { + + runTestHandlingFlakyAndCompilation( + "LegacyMongoPersonsEM", + 2000, + (args) -> { + setOption(args, "heuristicsForMongo", "true"); + setOption(args, "instrumentMR_MONGO", "true"); + setOption(args, "generateMongoData", "false"); + setOption(args, "extractMongoExecutionInfo", "true"); + + Solution solution = initAndRun(args); + + assertFalse(solution.getIndividuals().isEmpty()); + assertHasAtLeastOne(solution, HttpVerb.POST, 201, "/legacymongo/persons", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/legacymongo/persons", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/legacymongo/persons", null); + }); + } +}