diff --git a/.gitignore b/.gitignore index 6243070d1..602de1073 100644 --- a/.gitignore +++ b/.gitignore @@ -332,6 +332,9 @@ scripts/dockerize/dockerfiles /jdk_21_maven/cs/rest/redis-sample/target/ /jdk_21_maven/em/external/rest/redis-sample/target/ /jdk_21_maven/em/embedded/rest/redis-sample/target/ +/jdk_21_maven/cs/rest/joinus/target/ +/jdk_21_maven/em/external/rest/joinus/target/ +/jdk_21_maven/em/embedded/rest/joinus/target/ /jdk_11_maven/em/embedded/rest/tracking-system/target/ /jdk_11_maven/em/external/rest/tracking-system/target/ /jdk_17_maven/em/embedded/rest/tiltaksgjennomforing/target/ diff --git a/README.md b/README.md index 24ae27597..40f1018bc 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ How to setup authentication information, based on the current content of the ini Auth configuration files can found in the [auth](auth) folder. -### REST: Java/Kotlin (39) +### REST: Java/Kotlin (40) * **Bibliothek** (MIT), [jdk_17_gradle/cs/rest/bibliothek](jdk_17_gradle/cs/rest/bibliothek), from [https://github.com/PaperMC/bibliothek](https://github.com/PaperMC/bibliothek) @@ -111,6 +111,8 @@ Auth configuration files can found in the [auth](auth) folder. * **Jasper** (MIT), [jdk_25_maven/cs/rest/jasper](jdk_25_maven/cs/rest/jasper), from [https://github.com/cjmalloy/jasper](https://github.com/cjmalloy/jasper) +* **JoinUs** (not-known license), [jdk_21_maven/cs/rest/joinus](jdk_21_maven/cs/rest/joinus), from [https://github.com/KLAJDI16/joinUs](https://github.com/KLAJDI16/joinUs) + * **Languagetool** (LGPL), [jdk_8_maven/cs/rest/original/languagetool](jdk_8_maven/cs/rest/original/languagetool), from [https://github.com/languagetool-org/languagetool](https://github.com/languagetool-org/languagetool) * **Market** (MIT), [jdk_11_maven/cs/rest-gui/market](jdk_11_maven/cs/rest-gui/market), from [https://github.com/aleksey-lukyanets/market](https://github.com/aleksey-lukyanets/market) diff --git a/auth/joinus-auth.yaml b/auth/joinus-auth.yaml new file mode 100644 index 000000000..8b8c07b45 --- /dev/null +++ b/auth/joinus-auth.yaml @@ -0,0 +1,14 @@ +auth: + - name: wfd_admin + loginEndpointAuth: + payloadRaw: "{\"userid\": \"wfd_admin\", \"password\": \"Wfd-Pass1\"}" + - name: wfd_user + loginEndpointAuth: + payloadRaw: "{\"userid\": \"wfd_user\", \"password\": \"Wfd-Pass2\"}" + +authTemplate: + loginEndpointAuth: + endpoint: /login + verb: POST + contentType: application/json + expectCookies: true diff --git a/dockerfiles/joinus.dockerfile b/dockerfiles/joinus.dockerfile new file mode 100644 index 000000000..eb39cf25b --- /dev/null +++ b/dockerfiles/joinus.dockerfile @@ -0,0 +1,13 @@ +FROM amazoncorretto:21-alpine-jdk + +COPY ./dist/joinus-sut.jar . +COPY ./dist/jacocoagent.jar . + + + + +ENTRYPOINT \ + java \ + -javaagent:jacocoagent.jar=output=tcpserver,address=*,port=6300,append=false,dumponexit=false \ + -jar joinus-sut.jar \ + --server.port=8080 --spring.mongodb.uri=mongodb://db:27017/joinUs --spring.neo4j.uri=bolt://neo4j:7687 --spring.neo4j.authentication.username=neo4j --spring.neo4j.authentication.password=wfdNeo4jPass123 --logging.level.org.mongodb.driver=INFO --logging.level.org.neo4j.driver=INFO --logging.level.org.springframework.security=INFO \ No newline at end of file diff --git a/dockerfiles/joinus.yaml b/dockerfiles/joinus.yaml new file mode 100644 index 000000000..5ec8ab0e2 --- /dev/null +++ b/dockerfiles/joinus.yaml @@ -0,0 +1,56 @@ +services: + mitmproxy: + image: mitmproxy/mitmproxy:10.2.4 + user: "0:0" + environment: + MITM_LOG_DIR: /custom-logs + MITM_LOG_FILE: ${MITM_LOG_FILE:-minlog.csv} + command: > + mitmdump + --mode reverse:http://sut-joinus:8080 + --listen-host 0.0.0.0 + --listen-port 8080 + --set keep_host_header=true + -s /addons/minlog.py + volumes: + - ${HOST_LOG_DIR:-./mitm-logs}:/custom-logs + - ./addons:/addons:ro + ports: + - "${HOST_PORT:-8080}:8080" + depends_on: + - sut-joinus + sut-joinus: + image: webfuzzing/wfd-joinus:FINAL +# build: +# dockerfile: ./dockerfiles/joinus.dockerfile +# context: .. + environment: + AUTH_PORT: ${AUTH_PORT:-8081} + ports: + - "${JACOCO_PORT:-6300}:6300" + depends_on: + db: + condition: service_healthy + neo4j: + condition: service_healthy + db: + image: mongo:7 + volumes: + - ../scripts/dockerize/data/additional_files/joinus/joinus-mongo-seed.js:/docker-entrypoint-initdb.d/joinus-mongo-seed.js + healthcheck: + test: mongosh --quiet --eval "db.adminCommand('ping')" + interval: 30s + timeout: 30s + retries: 3 + neo4j: + image: neo4j:5.26 + environment: + NEO4J_AUTH: neo4j/wfdNeo4jPass123 + volumes: + - ../scripts/dockerize/data/additional_files/joinus/joinus-neo4j-seed.cypher:/seed.cypher + healthcheck: + test: cypher-shell -u neo4j -p wfdNeo4jPass123 "MATCH (:WfdSeedComplete) RETURN 1" + interval: 30s + timeout: 30s + retries: 3 + command: ["bash", "-c", '/startup/docker-entrypoint.sh neo4j & pid=$!; until cypher-shell -u neo4j -p wfdNeo4jPass123 "RETURN 1" >/dev/null 2>&1; do sleep 2; done; cypher-shell -u neo4j -p wfdNeo4jPass123 -f /seed.cypher; cypher-shell -u neo4j -p wfdNeo4jPass123 "CREATE (:WfdSeedComplete)"; wait $pid'] \ No newline at end of file diff --git a/experiments/bb-exp.py b/experiments/bb-exp.py index 62287e7f1..cad954c41 100644 --- a/experiments/bb-exp.py +++ b/experiments/bb-exp.py @@ -91,6 +91,7 @@ def __init__(self, name, auth, sleep, schemaformat, classfiles): "jdk_8_maven/cs/rest-gui/genome-nexus/web/target/classes"]), Sut("gestaohospital",False,SLEEP,JSON,["jdk_8_maven/cs/rest-gui/gestaohospital/target/classes"]), Sut("http-patch-spring",False,SLEEP,JSON,["jdk_11_maven/cs/rest/http-patch-spring/target/classes"]), + Sut("joinus",True,SLEEP,JSON,["jdk_21_maven/cs/rest/joinus/target/classes"]), Sut("languagetool",False,SLEEP,JSON,["jdk_8_maven/cs/rest/original/languagetool/languagetool-core/target/classes", "jdk_8_maven/cs/rest/original/languagetool/languagetool-gui-commons/target/classes", "jdk_8_maven/cs/rest/original/languagetool/languagetool-language-modules/all/target/classes", diff --git a/experiments/wb-exp.py b/experiments/wb-exp.py index f4afaa39a..d275faf23 100644 --- a/experiments/wb-exp.py +++ b/experiments/wb-exp.py @@ -194,6 +194,7 @@ def __init__(self, name, platform): Sut("gestaohospital", JDK_8), Sut("http-patch-spring", JDK_11), Sut("jasper", JDK_25), + Sut("joinus", JDK_21), Sut("languagetool", JDK_8), Sut("market", JDK_11), Sut("microcks", JDK_21), diff --git a/jacoco.dockerfile b/jacoco.dockerfile index 089691664..61b283121 100644 --- a/jacoco.dockerfile +++ b/jacoco.dockerfile @@ -27,6 +27,7 @@ COPY jdk_8_maven/cs/rest-gui/genome-nexus/service/target/classes ${CLASS_FILES} COPY jdk_8_maven/cs/rest-gui/genome-nexus/web/target/classes ${CLASS_FILES}/jdk_8_maven/cs/rest-gui/genome-nexus/web/target/classes COPY jdk_8_maven/cs/rest-gui/gestaohospital/target/classes ${CLASS_FILES}/jdk_8_maven/cs/rest-gui/gestaohospital/target/classes COPY jdk_11_maven/cs/rest/http-patch-spring/target/classes ${CLASS_FILES}/jdk_11_maven/cs/rest/http-patch-spring/target/classes +COPY jdk_21_maven/cs/rest/joinus/target/classes ${CLASS_FILES}/jdk_21_maven/cs/rest/joinus/target/classes COPY jdk_8_maven/cs/rest/original/languagetool/languagetool-core/target/classes ${CLASS_FILES}/jdk_8_maven/cs/rest/original/languagetool/languagetool-core/target/classes COPY jdk_8_maven/cs/rest/original/languagetool/languagetool-gui-commons/target/classes ${CLASS_FILES}/jdk_8_maven/cs/rest/original/languagetool/languagetool-gui-commons/target/classes COPY jdk_8_maven/cs/rest/original/languagetool/languagetool-language-modules/all/target/classes ${CLASS_FILES}/jdk_8_maven/cs/rest/original/languagetool/languagetool-language-modules/all/target/classes diff --git a/jdk_21_maven/cs/rest/joinus/pom.xml b/jdk_21_maven/cs/rest/joinus/pom.xml index db300c597..515ecf4ab 100644 --- a/jdk_21_maven/cs/rest/joinus/pom.xml +++ b/jdk_21_maven/cs/rest/joinus/pom.xml @@ -184,6 +184,12 @@ org.springframework.boot spring-boot-maven-plugin + + + joinus + sut + org.projectlombok diff --git a/jdk_21_maven/cs/rest/pom.xml b/jdk_21_maven/cs/rest/pom.xml index ada40d03e..615c1360b 100644 --- a/jdk_21_maven/cs/rest/pom.xml +++ b/jdk_21_maven/cs/rest/pom.xml @@ -13,6 +13,7 @@ + joinus person-controller redis-sample diff --git a/jdk_21_maven/em/embedded/rest/joinus/pom.xml b/jdk_21_maven/em/embedded/rest/joinus/pom.xml new file mode 100644 index 000000000..f47b46ed7 --- /dev/null +++ b/jdk_21_maven/em/embedded/rest/joinus/pom.xml @@ -0,0 +1,52 @@ + + + + 4.0.0 + + evomaster-benchmark-jdk21-em-embedded-rest-joinus + jar + + + org.evomaster + evomaster-benchmark-jdk21-em-embedded-rest + 4.3.1-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-dependencies + 4.0.1 + pom + import + + + + + + + + com.example + joinUs + 0.0.1-SNAPSHOT + + + + org.testcontainers + testcontainers + compile + + + + org.mongodb + mongodb-driver-sync + + + org.neo4j.driver + neo4j-java-driver + + + + diff --git a/jdk_21_maven/em/embedded/rest/joinus/src/main/java/em/embedded/com/example/joinUs/EmbeddedEvoMasterController.java b/jdk_21_maven/em/embedded/rest/joinus/src/main/java/em/embedded/com/example/joinUs/EmbeddedEvoMasterController.java new file mode 100644 index 000000000..66cc371ab --- /dev/null +++ b/jdk_21_maven/em/embedded/rest/joinus/src/main/java/em/embedded/com/example/joinUs/EmbeddedEvoMasterController.java @@ -0,0 +1,241 @@ +package em.embedded.com.example.joinUs; + +import com.example.joinUs.JoinUsApplication; +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.sql.DbSpecification; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import com.webfuzzing.commons.auth.LoginEndpoint; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Session; +import org.neo4j.driver.AuthTokens; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +/** + * Class used to start/stop the SUT. + */ +public class EmbeddedEvoMasterController extends EmbeddedSutController { + + private static final String MONGODB_IMAGE = "mongo:7"; + + private static final int MONGODB_PORT = 27017; + + private static final String MONGODB_DATABASE = "joinUs"; + + private static final String NEO4J_IMAGE = "neo4j:5.26"; + + private static final int NEO4J_BOLT_PORT = 7687; + + private static final String NEO4J_USERNAME = "neo4j"; + + private static final String NEO4J_PASSWORD = "wfdNeo4jPass123"; + + // Login is by member id, and only a document written directly to Mongo can carry isAdmin=true + // (/register always stores false). Same two accounts as the BB seed files. + private static final List USERS = List.of( + user("wfd_admin", "$2a$10$wMFYFpqtbELhPxBYXz/Cy.0qhbq/C1H0RRjVZKmSzdAulP8tCCtLS", true), + user("wfd_user", "$2a$10$p5FTsGJHk1g1pCncGxcgFeTJ4hxL8JZWibhVkuE6hVsOB8BeMi8nm", false) + ); + + private static Document user(String id, String passwordHash, boolean admin) { + return new Document("_id", id) + .append("member_name", id) + .append("bio", "") + .append("topics", List.of()) + .append("event_count", 0) + .append("group_count", 0) + .append("upcoming_events", List.of()) + .append("password", passwordHash) + .append("isAdmin", admin) + .append("_class", "com.example.joinUs.model.mongodb.User"); + } + + private static final GenericContainer mongodb = new GenericContainer(MONGODB_IMAGE) + .withExposedPorts(MONGODB_PORT); + + private static final GenericContainer neo4j = new GenericContainer(NEO4J_IMAGE) + .withEnv("NEO4J_AUTH", NEO4J_USERNAME + "/" + NEO4J_PASSWORD) + .withExposedPorts(NEO4J_BOLT_PORT) + .waitingFor(Wait.forLogMessage(".*Started.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)); + + public static void main(String[] args) { + + int port = 40100; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } + + EmbeddedEvoMasterController controller = new EmbeddedEvoMasterController(port); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + private ConfigurableApplicationContext ctx; + + private MongoClient mongoClient; + + private Driver neo4jDriver; + + public EmbeddedEvoMasterController() { + this(0); + } + + public EmbeddedEvoMasterController(int port) { + setControllerPort(port); + } + + private String mongodbUri() { + return "mongodb://" + mongodb.getHost() + ":" + mongodb.getMappedPort(MONGODB_PORT) + "/" + MONGODB_DATABASE; + } + + private String neo4jUri() { + return "bolt://" + neo4j.getHost() + ":" + neo4j.getMappedPort(NEO4J_BOLT_PORT); + } + + @Override + public String startSut() { + + mongodb.start(); + neo4j.start(); + mongoClient = MongoClients.create(mongodbUri()); + neo4jDriver = GraphDatabase.driver(neo4jUri(), AuthTokens.basic(NEO4J_USERNAME, NEO4J_PASSWORD)); + seedUsers(); + + ctx = SpringApplication.run(JoinUsApplication.class, new String[]{ + "--server.port=0", + "--spring.mongodb.uri=" + mongodbUri(), + "--spring.neo4j.uri=" + neo4jUri(), + "--spring.neo4j.authentication.username=" + NEO4J_USERNAME, + "--spring.neo4j.authentication.password=" + NEO4J_PASSWORD, + // application.properties logs every DB command at DEBUG + "--logging.level.org.mongodb.driver=INFO", + "--logging.level.org.neo4j.driver=INFO", + "--logging.level.org.springframework.security=INFO" + }); + + return "http://localhost:" + getSutPort(); + } + + private void seedUsers() { + mongoClient.getDatabase(MONGODB_DATABASE).getCollection("members").insertMany(USERS); + try (Session session = neo4jDriver.session()) { + session.run("UNWIND $ids AS id CREATE (:Member {member_id: id, member_name: id})", + Map.of("ids", USERS.stream().map(u -> u.getString("_id")).toList())).consume(); + } + } + + protected int getSutPort() { + return (Integer) ((Map) ctx.getEnvironment() + .getPropertySources().get("server.ports").getSource()) + .get("local.server.port"); + } + + @Override + public boolean isSutRunning() { + return ctx != null && ctx.isRunning(); + } + + @Override + public void stopSut() { + if (ctx != null) { + ctx.stop(); + ctx.close(); + ctx = null; + } + if (mongoClient != null) { + mongoClient.close(); + mongoClient = null; + } + if (neo4jDriver != null) { + neo4jDriver.close(); + neo4jDriver = null; + } + neo4j.stop(); + mongodb.stop(); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.example.joinUs."; + } + + // Reseeding also undoes DELETE /user/profile, which removes the logged-in account. Black-box runs + // have no reset, so there that endpoint has to be excluded (--endpointExclude /user/profile). + @Override + public void resetStateOfSUT() { + MongoDatabase db = mongoClient.getDatabase(MONGODB_DATABASE); + for (String name : db.listCollectionNames()) { + db.getCollection(name).deleteMany(new Document()); + } + try (Session session = neo4jDriver.session()) { + session.run("MATCH (n) DETACH DELETE n").consume(); + } + seedUsers(); + } + + // Lets EvoMaster compute its Mongo query heuristics; without it every test that touches a + // Mongo endpoint fails coverage retrieval with an NPE and forces a full SUT restart. + @Override + public Object getMongoConnection() { + return mongoClient; + } + + @Override + public List getDbSpecifications() { + return null; + } + + + // POST /login takes a JSON body and answers with a JSESSIONID cookie + private static AuthenticationDto cookieLogin(String name, String userId, String password) { + LoginEndpoint login = new LoginEndpoint(); + login.setEndpoint("/login"); + login.setVerb(LoginEndpoint.HttpVerb.POST); + login.setContentType("application/json"); + login.setPayloadRaw("{\"userid\":\"" + userId + "\",\"password\":\"" + password + "\"}"); + login.setExpectCookies(true); + AuthenticationDto dto = new AuthenticationDto(name); + dto.setLoginEndpointAuth(login); + return dto; + } + + @Override + public List getInfoForAuthentication() { + return List.of( + cookieLogin("WfdAdmin", "wfd_admin", "Wfd-Pass1"), + cookieLogin("WfdUser", "wfd_user", "Wfd-Pass2") + ); + } + + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + getSutPort() + "/v3/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + +} diff --git a/jdk_21_maven/em/embedded/rest/pom.xml b/jdk_21_maven/em/embedded/rest/pom.xml index ee9b2fbe5..9de242057 100644 --- a/jdk_21_maven/em/embedded/rest/pom.xml +++ b/jdk_21_maven/em/embedded/rest/pom.xml @@ -12,6 +12,7 @@ pom + joinus person-controller redis-sample diff --git a/jdk_21_maven/em/external/rest/joinus/pom.xml b/jdk_21_maven/em/external/rest/joinus/pom.xml new file mode 100644 index 000000000..7787b1cd9 --- /dev/null +++ b/jdk_21_maven/em/external/rest/joinus/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + evomaster-benchmark-jdk21-em-external-rest-joinus + jar + + + org.evomaster + evomaster-benchmark-jdk21-em-external-rest + 4.3.1-SNAPSHOT + + + + + + org.testcontainers + testcontainers + compile + + + + org.mongodb + mongodb-driver-sync + 5.6.4 + + + org.neo4j.driver + neo4j-java-driver + 6.0.3 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.2 + + + package + + shade + + + joinus-evomaster-runner + + + + em.external.com.example.joinUs.ExternalEvoMasterController + + org.evomaster.client.java.instrumentation.InstrumentingAgent + + org.evomaster.client.java.instrumentation.InstrumentingAgent + + true + true + + + + + + org.slf4j + shaded.emb.org.slf4j + + + com.fasterxml.jackson.annotation + shaded.emb.com.fasterxml.jackson.annotation + + + + + + + + + diff --git a/jdk_21_maven/em/external/rest/joinus/src/main/java/em/external/com/example/joinUs/ExternalEvoMasterController.java b/jdk_21_maven/em/external/rest/joinus/src/main/java/em/external/com/example/joinUs/ExternalEvoMasterController.java new file mode 100644 index 000000000..6bdd8fa63 --- /dev/null +++ b/jdk_21_maven/em/external/rest/joinus/src/main/java/em/external/com/example/joinUs/ExternalEvoMasterController.java @@ -0,0 +1,297 @@ +package em.external.com.example.joinUs; + +import org.evomaster.client.java.controller.ExternalSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.sql.DbSpecification; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import com.webfuzzing.commons.auth.LoginEndpoint; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Session; +import org.neo4j.driver.AuthTokens; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +public class ExternalEvoMasterController extends ExternalSutController { + + private static final int DEFAULT_CONTROLLER_PORT = 40100; + + private static final int DEFAULT_SUT_PORT = 12345; + + private static final String MONGODB_IMAGE = "mongo:7"; + + private static final int MONGODB_PORT = 27017; + + private static final String MONGODB_DATABASE = "joinUs"; + + private static final String NEO4J_IMAGE = "neo4j:5.26"; + + private static final int NEO4J_BOLT_PORT = 7687; + + private static final String NEO4J_USERNAME = "neo4j"; + + private static final String NEO4J_PASSWORD = "wfdNeo4jPass123"; + + // Login is by member id, and only a document written directly to Mongo can carry isAdmin=true + // (/register always stores false). Same two accounts as the BB seed files. + private static final List USERS = List.of( + user("wfd_admin", "$2a$10$wMFYFpqtbELhPxBYXz/Cy.0qhbq/C1H0RRjVZKmSzdAulP8tCCtLS", true), + user("wfd_user", "$2a$10$p5FTsGJHk1g1pCncGxcgFeTJ4hxL8JZWibhVkuE6hVsOB8BeMi8nm", false) + ); + + private static Document user(String id, String passwordHash, boolean admin) { + return new Document("_id", id) + .append("member_name", id) + .append("bio", "") + .append("topics", List.of()) + .append("event_count", 0) + .append("group_count", 0) + .append("upcoming_events", List.of()) + .append("password", passwordHash) + .append("isAdmin", admin) + .append("_class", "com.example.joinUs.model.mongodb.User"); + } + + private static final GenericContainer mongodb = new GenericContainer(MONGODB_IMAGE) + .withExposedPorts(MONGODB_PORT); + + private static final GenericContainer neo4j = new GenericContainer(NEO4J_IMAGE) + .withEnv("NEO4J_AUTH", NEO4J_USERNAME + "/" + NEO4J_PASSWORD) + .withExposedPorts(NEO4J_BOLT_PORT) + .waitingFor(Wait.forLogMessage(".*Started.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)); + + public static void main(String[] args) { + + int controllerPort = DEFAULT_CONTROLLER_PORT; + if (args.length > 0) { + controllerPort = Integer.parseInt(args[0]); + } + int sutPort = DEFAULT_SUT_PORT; + if (args.length > 1) { + sutPort = Integer.parseInt(args[1]); + } + String jarLocation = "cs/rest/joinus/target"; + if (args.length > 2) { + jarLocation = args[2]; + } + if (!jarLocation.endsWith(".jar")) { + jarLocation += "/joinus-sut.jar"; + } + + int timeoutSeconds = 120; + if (args.length > 3) { + timeoutSeconds = Integer.parseInt(args[3]); + } + + String command = "java"; + if (args.length > 4) { + command = args[4]; + } + + ExternalEvoMasterController controller = + new ExternalEvoMasterController(controllerPort, jarLocation, sutPort, timeoutSeconds, command); + + controller.setNeedsJdk17Options(true); + + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + + private final int timeoutSeconds; + + private final int sutPort; + + private String jarLocation; + + private MongoClient mongoClient; + + private Driver neo4jDriver; + + public ExternalEvoMasterController() { + this(DEFAULT_CONTROLLER_PORT, "../target/joinus-sut.jar", DEFAULT_SUT_PORT, 120, "java"); + } + + public ExternalEvoMasterController(String jarLocation) { + this(); + this.jarLocation = jarLocation; + } + + public ExternalEvoMasterController(int controllerPort, String jarLocation, int sutPort, int timeoutSeconds, String command) { + this.sutPort = sutPort; + this.jarLocation = jarLocation; + this.timeoutSeconds = timeoutSeconds; + + setControllerPort(controllerPort); + setJavaCommand(command); + } + + private String mongodbUri() { + return "mongodb://" + mongodb.getHost() + ":" + mongodb.getMappedPort(MONGODB_PORT) + "/" + MONGODB_DATABASE; + } + + private String neo4jUri() { + return "bolt://" + neo4j.getHost() + ":" + neo4j.getMappedPort(NEO4J_BOLT_PORT); + } + + @Override + public String[] getInputParameters() { + return new String[]{ + "--server.port=" + sutPort, + "--spring.mongodb.uri=" + mongodbUri(), + "--spring.neo4j.uri=" + neo4jUri(), + "--spring.neo4j.authentication.username=" + NEO4J_USERNAME, + "--spring.neo4j.authentication.password=" + NEO4J_PASSWORD, + // application.properties logs every DB command at DEBUG + "--logging.level.org.mongodb.driver=INFO", + "--logging.level.org.neo4j.driver=INFO", + "--logging.level.org.springframework.security=INFO" + }; + } + + @Override + public String[] getJVMParameters() { + return new String[]{}; + } + + + @Override + public String getBaseURL() { + return "http://localhost:" + sutPort; + } + + @Override + public String getPathToExecutableJar() { + return jarLocation; + } + + @Override + public String getLogMessageOfInitializedServer() { + return "Started JoinUsApplication in "; + } + + @Override + public long getMaxAwaitForInitializationInSeconds() { + return timeoutSeconds; + } + + @Override + public void preStart() { + mongodb.start(); + neo4j.start(); + mongoClient = MongoClients.create(mongodbUri()); + neo4jDriver = GraphDatabase.driver(neo4jUri(), AuthTokens.basic(NEO4J_USERNAME, NEO4J_PASSWORD)); + seedUsers(); + } + + private void seedUsers() { + mongoClient.getDatabase(MONGODB_DATABASE).getCollection("members").insertMany(USERS); + try (Session session = neo4jDriver.session()) { + session.run("UNWIND $ids AS id CREATE (:Member {member_id: id, member_name: id})", + Map.of("ids", USERS.stream().map(u -> u.getString("_id")).toList())).consume(); + } + } + + @Override + public void postStart() { + } + + @Override + public void preStop() { + } + + @Override + public void postStop() { + if (mongoClient != null) { + mongoClient.close(); + mongoClient = null; + } + if (neo4jDriver != null) { + neo4jDriver.close(); + neo4jDriver = null; + } + neo4j.stop(); + mongodb.stop(); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.example.joinUs."; + } + + // Reseeding also undoes DELETE /user/profile, which removes the logged-in account. Black-box runs + // have no reset, so there that endpoint has to be excluded (--endpointExclude /user/profile). + @Override + public void resetStateOfSUT() { + MongoDatabase db = mongoClient.getDatabase(MONGODB_DATABASE); + for (String name : db.listCollectionNames()) { + db.getCollection(name).deleteMany(new Document()); + } + try (Session session = neo4jDriver.session()) { + session.run("MATCH (n) DETACH DELETE n").consume(); + } + seedUsers(); + } + + // Lets EvoMaster compute its Mongo query heuristics; without it every test that touches a + // Mongo endpoint fails coverage retrieval with an NPE and forces a full SUT restart. + @Override + public Object getMongoConnection() { + return mongoClient; + } + + @Override + public List getDbSpecifications() { + return null; + } + + + // POST /login takes a JSON body and answers with a JSESSIONID cookie + private static AuthenticationDto cookieLogin(String name, String userId, String password) { + LoginEndpoint login = new LoginEndpoint(); + login.setEndpoint("/login"); + login.setVerb(LoginEndpoint.HttpVerb.POST); + login.setContentType("application/json"); + login.setPayloadRaw("{\"userid\":\"" + userId + "\",\"password\":\"" + password + "\"}"); + login.setExpectCookies(true); + AuthenticationDto dto = new AuthenticationDto(name); + dto.setLoginEndpointAuth(login); + return dto; + } + + @Override + public List getInfoForAuthentication() { + return List.of( + cookieLogin("WfdAdmin", "wfd_admin", "Wfd-Pass1"), + cookieLogin("WfdUser", "wfd_user", "Wfd-Pass2") + ); + } + + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + getBaseURL() + "/v3/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + +} diff --git a/jdk_21_maven/em/external/rest/pom.xml b/jdk_21_maven/em/external/rest/pom.xml index 3e059511d..f565fe4b5 100644 --- a/jdk_21_maven/em/external/rest/pom.xml +++ b/jdk_21_maven/em/external/rest/pom.xml @@ -13,6 +13,7 @@ + joinus person-controller redis-sample diff --git a/openapi/joinus.json b/openapi/joinus.json new file mode 100644 index 000000000..2a381be17 --- /dev/null +++ b/openapi/joinus.json @@ -0,0 +1,1883 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost:18080", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "Admin", + "description": "Admin-only analytics " + } + ], + "paths": { + "/user/profile": { + "get": { + "tags": [ + "User" + ], + "operationId": "getUserProfile", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "put": { + "tags": [ + "User" + ], + "operationId": "editUserProfile", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "delete": { + "tags": [ + "User" + ], + "operationId": "deleteUserProfile", + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/register": { + "post": { + "tags": [ + "Auth" + ], + "operationId": "register", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/logout": { + "post": { + "tags": [ + "Auth" + ], + "operationId": "logout", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/login": { + "post": { + "tags": [ + "Auth" + ], + "operationId": "login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginForm" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/groups": { + "get": { + "tags": [ + "Groups" + ], + "operationId": "getAllGroups", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageGroupSummaryDTO" + } + } + } + } + } + }, + "post": { + "tags": [ + "Groups" + ], + "operationId": "createGroup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupDTO" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/groups/{groupId}/organizers/{organizerId}": { + "post": { + "tags": [ + "Groups" + ], + "operationId": "addOrganizer", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "organizerId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/groups/{groupId}/leave": { + "post": { + "tags": [ + "Groups" + ], + "operationId": "leaveGroup", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/groups/{groupId}/join": { + "post": { + "tags": [ + "Groups" + ], + "operationId": "joinGroup", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/events": { + "get": { + "tags": [ + "Events" + ], + "operationId": "getAllEvents", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageEventSummaryDTO" + } + } + } + } + } + }, + "post": { + "tags": [ + "Events" + ], + "operationId": "createEvent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/groups/{id}": { + "get": { + "tags": [ + "Groups" + ], + "operationId": "getGroupById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/GroupDTO" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Groups" + ], + "operationId": "deleteGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Groups" + ], + "operationId": "updateGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/events/{id}": { + "get": { + "tags": [ + "Events" + ], + "operationId": "getEventById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/EventDTO" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Events" + ], + "operationId": "deleteEvent", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Events" + ], + "operationId": "editEvent", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/events/{id}/revokeAttendance": { + "patch": { + "tags": [ + "Events" + ], + "operationId": "revokeAttendance", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/events/{id}/attend": { + "patch": { + "tags": [ + "Events" + ], + "operationId": "attendEvent", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/user/allGroups": { + "get": { + "tags": [ + "User" + ], + "summary": "Get all groups (upcoming and historical) of the authenticated user", + "operationId": "getMyGroups", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupDTO" + } + } + } + } + } + } + } + }, + "/user/allEvents": { + "get": { + "tags": [ + "User" + ], + "summary": "Get all events (upcoming and historical) of the authenticated user", + "operationId": "getMyEvents", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventSummaryDTO" + } + } + } + } + } + } + } + }, + "/recommendations/groups/by-topics": { + "get": { + "tags": [ + "Recommendations" + ], + "summary": "Recommend groups based on the user's topics", + "operationId": "recommendGroupsByTopics", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupDTO" + } + } + } + } + } + } + } + }, + "/recommendations/groups/by-similar-members": { + "get": { + "tags": [ + "Recommendations" + ], + "summary": "Recommend groups joined by members similar to the authenticated user", + "operationId": "recommendGroupsBySimilarMembers", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupDTO" + } + } + } + } + } + } + } + }, + "/recommendations/events/by-peer-group-attendance": { + "get": { + "tags": [ + "Recommendations" + ], + "summary": "Recommend events attended by members of the user's groups", + "operationId": "recommendEventsByPeerGroupAttendance", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventSummaryDTO" + } + } + } + } + } + } + } + }, + "/recommendations/events/by-members": { + "get": { + "tags": [ + "Recommendations" + ], + "summary": "Recommend events attended by members with similar attendance patterns", + "operationId": "recommendEventsByMembers", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventSummaryDTO" + } + } + } + } + } + } + } + }, + "/recommendations/events/by-group-topics": { + "get": { + "tags": [ + "Recommendations" + ], + "summary": "Recommend events related to the user's group topics", + "operationId": "recommendEventsBySharedGroupTopics", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventSummaryDTO" + } + } + } + } + } + } + } + }, + "/groups/{groupId}/events": { + "get": { + "tags": [ + "Groups" + ], + "summary": "Get all events (historical and upcoming) organized by a group", + "operationId": "getEventsOrganizedByGroup", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventSummaryDTO" + } + } + } + } + } + } + } + }, + "/groups/search": { + "get": { + "tags": [ + "Groups" + ], + "operationId": "searchGroups", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "city", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "minMembers", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxMembers", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minEvents", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxEvents", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageGroupSummaryDTO" + } + } + } + } + } + } + }, + "/events/search": { + "get": { + "tags": [ + "Events" + ], + "operationId": "searchEvents", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "city", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "minMembers", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxMembers", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fromDate", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2026-12-15T01:45:30Z" + }, + { + "name": "toDate", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2027-12-15T01:45:30Z" + }, + { + "name": "maxFee", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageEventSummaryDTO" + } + } + } + } + } + } + }, + "/admin/graph/groups/communities": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Find group communities (Neo4j)", + "description": "Finds related group communities using Neo4j by measuring how many members groups share. sharedMembers is the minimum overlap; limit caps the results.", + "operationId": "findGroupCommunities", + "parameters": [ + { + "name": "sharedMembers", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 5 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupCommunityDTO" + } + } + } + } + } + } + } + }, + "/admin/analytics/users/activity-score": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Top users by activity score", + "description": "Returns the most active users based on a computed activity score (for example: eventCount + groupCount + other signals).", + "operationId": "topUsersByActivityScore", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ActivityScorePerUserAnalytic" + } + } + } + } + } + } + } + }, + "/admin/analytics/topics/trending-per-city": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Top trending topics per city", + "description": "For each city, returns the top topics found in users' profiles, along with how many users have each topic. topicCount controls how many topics per city are returned.", + "operationId": "topTrendingTopicsPerCity", + "parameters": [ + { + "name": "topicCount", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 5 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrendingTopicPerCityAnalytic" + } + } + } + } + } + } + } + }, + "/admin/analytics/events/paid-vs-free": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Paid vs Free event popularity", + "description": "Compares FREE vs PAID events using totalAttendance, eventsCount, and avgAttendance. An event is FREE if fee is missing/null or fee.amount is 0.", + "operationId": "paidVsFreePopularity", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/admin/analytics/cities/activity": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Most active cities", + "description": "Ranks cities by activityRatio (activeMembers/totalMembers). A user is considered active if they have at least one upcoming event.", + "operationId": "mostActiveCities", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CityActivityAnalytic" + } + } + } + } + } + } + } + }, + "/": { + "get": { + "tags": [ + "Auth" + ], + "operationId": "redirect", + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "CityEmbedded": { + "type": "object", + "properties": { + "cityId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "country": { + "type": "string" + }, + "state": { + "type": "string" + } + } + }, + "EventEmbedded": { + "type": "object", + "properties": { + "eventId": { + "type": "string" + }, + "eventName": { + "type": "string" + }, + "eventTime": { + "type": "string", + "format": "date-time" + } + } + }, + "TopicEmbedded": { + "type": "object", + "properties": { + "topicId": { + "type": "string" + }, + "topicName": { + "type": "string" + } + } + }, + "UserDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "memberName": { + "type": "string" + }, + "password": { + "type": "string" + }, + "city": { + "$ref": "#/components/schemas/CityEmbedded" + }, + "bio": { + "type": "string" + }, + "topics": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TopicEmbedded" + } + }, + "eventCount": { + "type": "integer", + "format": "int32" + }, + "groupCount": { + "type": "integer", + "format": "int32" + }, + "upcomingEvents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventEmbedded" + } + } + } + }, + "LoginForm": { + "type": "object", + "properties": { + "userid": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "Category": { + "type": "object", + "properties": { + "categoryId": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "GroupDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "city": { + "$ref": "#/components/schemas/CityEmbedded" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "memberCount": { + "type": "integer", + "format": "int32" + }, + "eventCount": { + "type": "integer", + "format": "int32" + }, + "organizers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserEmbedded" + } + }, + "upcomingEvents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventEmbedded" + } + }, + "topics": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TopicEmbedded" + } + }, + "groupPhoto": { + "$ref": "#/components/schemas/GroupPhotoDTO" + } + } + }, + "GroupPhotoDTO": { + "type": "object", + "properties": { + "highresLink": { + "type": "string" + }, + "photoLink": { + "type": "string" + }, + "thumbLink": { + "type": "string" + } + } + }, + "UserEmbedded": { + "type": "object", + "properties": { + "memberId": { + "type": "string" + }, + "memberName": { + "type": "string" + } + } + }, + "EventDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "eventName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "eventTime": { + "type": "string", + "format": "date-time" + }, + "updated": { + "type": "string", + "format": "date-time" + }, + "duration": { + "type": "integer", + "format": "int32" + }, + "fee": { + "$ref": "#/components/schemas/Fee" + }, + "venue": { + "$ref": "#/components/schemas/Venue" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "memberCount": { + "type": "integer", + "format": "int32" + }, + "creatorGroup": { + "$ref": "#/components/schemas/GroupEmbedded" + } + } + }, + "Fee": { + "type": "object", + "properties": { + "accepts": { + "type": "string" + }, + "amount": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string" + }, + "isRequired": { + "type": "boolean" + } + } + }, + "GroupEmbedded": { + "type": "object", + "properties": { + "groupId": { + "type": "string" + }, + "groupName": { + "type": "string" + } + } + }, + "Venue": { + "type": "object", + "properties": { + "city": { + "$ref": "#/components/schemas/CityEmbedded" + }, + "address1": { + "type": "string" + }, + "address2": { + "type": "string" + } + } + }, + "EventSummaryDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "eventName": { + "type": "string" + }, + "eventTime": { + "type": "string", + "format": "date-time" + }, + "updated": { + "type": "string", + "format": "date-time" + }, + "memberCount": { + "type": "integer", + "format": "int32" + }, + "feeAmount": { + "type": "integer", + "format": "int32" + }, + "venueCityName": { + "type": "string" + }, + "venueAddress1": { + "type": "string" + }, + "creatorGroup": { + "$ref": "#/components/schemas/GroupEmbedded" + } + } + }, + "GroupSummaryDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "cityName": { + "type": "string" + }, + "organizers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserEmbedded" + } + }, + "upcomingEvents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventEmbedded" + } + }, + "memberCount": { + "type": "integer", + "format": "int32" + }, + "eventCount": { + "type": "integer", + "format": "int32" + } + } + }, + "PageGroupSummaryDTO": { + "type": "object", + "properties": { + "totalElements": { + "type": "integer", + "format": "int64" + }, + "totalPages": { + "type": "integer", + "format": "int32" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupSummaryDTO" + } + }, + "number": { + "type": "integer", + "format": "int32" + }, + "sort": { + "$ref": "#/components/schemas/SortObject" + }, + "numberOfElements": { + "type": "integer", + "format": "int32" + }, + "pageable": { + "$ref": "#/components/schemas/PageableObject" + }, + "first": { + "type": "boolean" + }, + "last": { + "type": "boolean" + }, + "empty": { + "type": "boolean" + } + } + }, + "PageableObject": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64" + }, + "sort": { + "$ref": "#/components/schemas/SortObject" + }, + "paged": { + "type": "boolean" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "pageNumber": { + "type": "integer", + "format": "int32" + }, + "unpaged": { + "type": "boolean" + } + } + }, + "SortObject": { + "type": "object", + "properties": { + "empty": { + "type": "boolean" + }, + "unsorted": { + "type": "boolean" + }, + "sorted": { + "type": "boolean" + } + } + }, + "PageEventSummaryDTO": { + "type": "object", + "properties": { + "totalElements": { + "type": "integer", + "format": "int64" + }, + "totalPages": { + "type": "integer", + "format": "int32" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventSummaryDTO" + } + }, + "number": { + "type": "integer", + "format": "int32" + }, + "sort": { + "$ref": "#/components/schemas/SortObject" + }, + "numberOfElements": { + "type": "integer", + "format": "int32" + }, + "pageable": { + "$ref": "#/components/schemas/PageableObject" + }, + "first": { + "type": "boolean" + }, + "last": { + "type": "boolean" + }, + "empty": { + "type": "boolean" + } + } + }, + "GroupCommunityDTO": { + "type": "object", + "properties": { + "group1": { + "$ref": "#/components/schemas/GroupNeo4J" + }, + "group2": { + "$ref": "#/components/schemas/GroupNeo4J" + }, + "sharedMembers": { + "type": "integer", + "format": "int64" + } + } + }, + "GroupNeo4J": { + "type": "object", + "properties": { + "groupId": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "cityName": { + "type": "string" + }, + "categoryName": { + "type": "string" + } + } + }, + "ActivityScorePerUserAnalytic": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "userName": { + "type": "string" + }, + "activityScore": { + "type": "integer", + "format": "int64" + } + } + }, + "TopicPerUserAnalytic": { + "type": "object", + "properties": { + "topic": { + "type": "string" + }, + "usersCount": { + "type": "integer", + "format": "int32" + } + } + }, + "TrendingTopicPerCityAnalytic": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "topTopics": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TopicPerUserAnalytic" + } + } + } + }, + "CityActivityAnalytic": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "totalMembers": { + "type": "integer", + "format": "int32" + }, + "activeMembers": { + "type": "integer", + "format": "int32" + }, + "activityRatio": { + "type": "number", + "format": "double" + } + } + } + } + } +} diff --git a/scripts/build/docker-compose.build.yml b/scripts/build/docker-compose.build.yml index 645dfd930..b5b5ba850 100644 --- a/scripts/build/docker-compose.build.yml +++ b/scripts/build/docker-compose.build.yml @@ -256,11 +256,13 @@ services: fi cp cs/rest/person-controller/target/person-controller-sut.jar /dist/ cp cs/rest/redis-sample/target/redis-sample-sut.jar /dist/ + cp cs/rest/joinus/target/joinus-sut.jar /dist/ cp cs/rest-gui/webgoat/target/webgoat-sut.jar /dist/ cp cs/rest-gui/microcks/webapp/target/microcks-sut.jar /dist/ if [ "$${BUILD_EVOMASTER:-false}" = "true" ]; then cp em/external/rest/person-controller/target/person-controller-evomaster-runner.jar /dist/ cp em/external/rest/redis-sample/target/redis-sample-evomaster-runner.jar /dist/ + cp em/external/rest/joinus/target/joinus-evomaster-runner.jar /dist/ cp em/external/rest-gui/webgoat/target/webgoat-evomaster-runner.jar /dist/ cp em/external/rest-gui/microcks/target/microcks-evomaster-runner.jar /dist/ fi diff --git a/scripts/dist-wb.py b/scripts/dist-wb.py index 55a7aaa8b..5f4eb3a3d 100755 --- a/scripts/dist-wb.py +++ b/scripts/dist-wb.py @@ -297,6 +297,9 @@ def build_jdk_21_maven(): copy(folder + "/cs/rest/redis-sample/target/redis-sample-sut.jar", DIST) copy(folder + "/em/external/rest/redis-sample/target/redis-sample-evomaster-runner.jar", DIST) + copy(folder + "/cs/rest/joinus/target/joinus-sut.jar", DIST) + copy(folder + "/em/external/rest/joinus/target/joinus-evomaster-runner.jar", DIST) + copy(folder + "/cs/rest-gui/webgoat/target/webgoat-sut.jar", DIST) copy(folder + "/em/external/rest-gui/webgoat/target/webgoat-evomaster-runner.jar", DIST) diff --git a/scripts/dockerize/data/additional_files/joinus/joinus-mongo-seed.js b/scripts/dockerize/data/additional_files/joinus/joinus-mongo-seed.js new file mode 100644 index 000000000..17cd2955c --- /dev/null +++ b/scripts/dockerize/data/additional_files/joinus/joinus-mongo-seed.js @@ -0,0 +1,29 @@ +// MODIFIED (WFD-added, not part of the original SUT): the two known-credential accounts the +// drivers seed. Login is by member id, and only a direct write can set isAdmin=true. +db = db.getSiblingDB('joinUs'); +db.getCollection('members').insertMany([ + { + "_id": "wfd_admin", + "member_name": "wfd_admin", + "bio": "", + "topics": [], + "event_count": 0, + "group_count": 0, + "upcoming_events": [], + "password": "$2a$10$wMFYFpqtbELhPxBYXz/Cy.0qhbq/C1H0RRjVZKmSzdAulP8tCCtLS", + "isAdmin": true, + "_class": "com.example.joinUs.model.mongodb.User" + }, + { + "_id": "wfd_user", + "member_name": "wfd_user", + "bio": "", + "topics": [], + "event_count": 0, + "group_count": 0, + "upcoming_events": [], + "password": "$2a$10$p5FTsGJHk1g1pCncGxcgFeTJ4hxL8JZWibhVkuE6hVsOB8BeMi8nm", + "isAdmin": false, + "_class": "com.example.joinUs.model.mongodb.User" + } +]); diff --git a/scripts/dockerize/data/additional_files/joinus/joinus-neo4j-seed.cypher b/scripts/dockerize/data/additional_files/joinus/joinus-neo4j-seed.cypher new file mode 100644 index 000000000..2b430b059 --- /dev/null +++ b/scripts/dockerize/data/additional_files/joinus/joinus-neo4j-seed.cypher @@ -0,0 +1,4 @@ +// MODIFIED (WFD-added, not part of the original SUT): the two known-credential accounts the +// drivers seed. Login is by member id, and only a direct write can set isAdmin=true. +CREATE (:Member {member_id: 'wfd_admin', member_name: 'wfd_admin'}); +CREATE (:Member {member_id: 'wfd_user', member_name: 'wfd_user'}); diff --git a/scripts/dockerize/data/sut.csv b/scripts/dockerize/data/sut.csv index 9476c6d79..4c06765f7 100644 --- a/scripts/dockerize/data/sut.csv +++ b/scripts/dockerize/data/sut.csv @@ -38,3 +38,4 @@ microcks,TRUE,-DPOSTMAN_RUNNER_URL=http://postman:3000 -DSERVICES_UPDATE_INTERVA commafeed,TRUE,"-Dquarkus.http.port=8080 -Dquarkus.datasource.db-kind=h2 -Dquarkus.datasource.jdbc.url=""jdbc:h2:mem:commafeed;DB_CLOSE_DELAY=-1"" -Dquarkus.datasource.username=sa -Dquarkus.datasource.password=sa","",http://localhost:8080/openapi?format=json,http://localhost:8080,FALSE,"","" jasper,TRUE,,--server.port=8080 --spring.profiles.active=api-docs --spring.datasource.url=jdbc:postgresql://db:5432/jasper --spring.datasource.username=postgres --spring.datasource.password=password --spring.datasource.hikari.auto-commit=false --spring.jpa.database-platform=jasper.config.PostgreSQLDialect --springdoc.api-docs.path=/api/v3/api-docs --jasper.allow-user-tag-header=true --jasper.allow-user-role-header=true --jasper.allow-auth-headers=true,http://localhost:8080/api/v3/api-docs/jasper,http://localhost:8080,FALSE,,"[{""image_name"": ""postgres:18"", ""tmp_fs"": ""/var/lib/postgresql"", ""environment"": ""POSTGRES_PASSWORD: password;POSTGRES_HOST_AUTH_METHOD: trust;POSTGRES_DB: jasper"", ""volume"": """", ""health_check_command"": """"}]" redis-sample,TRUE,"",--server.port=8080 --spring.data.redis.host=db --spring.data.redis.port=6379 --spring.data.redis.password=53cret --spring.autoconfigure.exclude=org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration --app.numberOfRatings=5000 --app.ratingStars=5,http://localhost:8080/v3/api-docs,http://localhost:8080,FALSE,"","[{""image_name"": ""redis:7.4-alpine"", ""tmp_fs"": """", ""environment"": """", ""volume"": """", ""health_check_command"": """", ""command"": ""redis-server --requirepass 53cret""}]" +joinus,TRUE,,--server.port=8080 --spring.mongodb.uri=mongodb://db:27017/joinUs --spring.neo4j.uri=bolt://neo4j:7687 --spring.neo4j.authentication.username=neo4j --spring.neo4j.authentication.password=wfdNeo4jPass123 --logging.level.org.mongodb.driver=INFO --logging.level.org.neo4j.driver=INFO --logging.level.org.springframework.security=INFO,http://localhost:8080/v3/api-docs,http://localhost:8080,FALSE,db;neo4j,"[{""image_name"": ""mongo:7"", ""tmp_fs"": """", ""environment"": """", ""volume"": ""../scripts/dockerize/data/additional_files/joinus/joinus-mongo-seed.js:/docker-entrypoint-initdb.d/joinus-mongo-seed.js"", ""health_check_command"": ""mongosh --quiet --eval \""db.adminCommand('ping')\""""}, {""name"": ""neo4j"", ""image_name"": ""neo4j:5.26"", ""tmp_fs"": """", ""environment"": ""NEO4J_AUTH: neo4j/wfdNeo4jPass123"", ""volume"": ""../scripts/dockerize/data/additional_files/joinus/joinus-neo4j-seed.cypher:/seed.cypher"", ""health_check_command"": ""cypher-shell -u neo4j -p wfdNeo4jPass123 \""MATCH (:WfdSeedComplete) RETURN 1\"""", ""command"": ""[\""bash\"", \""-c\"", '/startup/docker-entrypoint.sh neo4j & pid=$!; until cypher-shell -u neo4j -p wfdNeo4jPass123 \""RETURN 1\"" >/dev/null 2>&1; do sleep 2; done; cypher-shell -u neo4j -p wfdNeo4jPass123 -f /seed.cypher; cypher-shell -u neo4j -p wfdNeo4jPass123 \""CREATE (:WfdSeedComplete)\""; wait $pid']""}]" diff --git a/statistics/data.csv b/statistics/data.csv index 80b1fbd4a..56c0b6ca4 100644 --- a/statistics/data.csv +++ b/statistics/data.csv @@ -47,6 +47,7 @@ TRUE,signal-registration,gRPC,Java,JDK 17,Maven,177,13652,,UNDEFINED,5,FALSE,htt TRUE,webgoat,REST,Java,JDK 21,Maven,355,27638,H2,GPL,204,TRUE,https://github.com/WebGoat/WebGoat TRUE,commafeed,REST,Java,JDK 25,Maven,201,21287,H2,Apache,44,TRUE,https://github.com/Athou/commafeed TRUE,jasper,REST,Java,JDK 25,Maven,337,46791,PostgreSQL,MIT,61,TRUE,https://github.com/cjmalloy/jasper +TRUE,joinus,REST,Java,JDK 21,Maven,101,7906,MongoDB;Neo4j,UNDEFINED,37,TRUE,https://github.com/KLAJDI16/joinUs TRUE,redis-sample,REST,Java,JDK 21,Maven,18,1006,Redis,UNDEFINED,4,FALSE,https://github.com/hendisantika/spring-boot-redis-sample FALSE,ind0,REST,Java,JDK 8,Gradle,103,17039,PostgreSQL,Proprietary,20,FALSE,UNDEFINED FALSE,ind1,REST,Java;Kotlin,JDK 11,Gradle,163,15240,PostgreSQL,Proprietary,53,FALSE,UNDEFINED diff --git a/statistics/table_emb.md b/statistics/table_emb.md index de751d2e8..9c55da36f 100644 --- a/statistics/table_emb.md +++ b/statistics/table_emb.md @@ -20,6 +20,7 @@ |REST|__gestaohospital__|3506|33|20|Java|JDK 8|Maven|MongoDB|| |REST|__http-patch-spring__|1450|30|6|Java|JDK 11|Maven||| |REST|__jasper__|46791|337|61|Java|JDK 25|Maven|PostgreSQL|✓| +|REST|__joinus__|7906|101|37|Java|JDK 21|Maven|MongoDB, Neo4j|✓| |REST|__languagetool__|174781|1385|2|Java|JDK 8|Maven||| |REST|__market__|9861|124|13|Java|JDK 11|Maven|H2|✓| |REST|__microcks__|66186|471|88|Java|JDK 21|Maven|MongoDB|✓|