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|✓|