diff --git a/admin/src/main/ts/package.json b/admin/src/main/ts/package.json index d415b72d91..61b285dcfc 100644 --- a/admin/src/main/ts/package.json +++ b/admin/src/main/ts/package.json @@ -1,6 +1,6 @@ { "name": "admin-app", - "version": "6.15.6", + "version": "6.15.6-%branch%.%generateVersion%", "scripts": { "ng": "ng", "start": "ng serve --host 0.0.0.0", @@ -34,9 +34,9 @@ "font-awesome": "4.7.0", "jquery": "^3.4.1", "ngx-infinite-scroll": "14.0.1", - "ngx-ode-core": "4.7.4", - "ngx-ode-sijil": "4.7.4", - "ngx-ode-ui": "4.7.4", + "ngx-ode-core": "dev", + "ngx-ode-sijil": "dev", + "ngx-ode-ui": "dev", "ngx-trumbowyg": "^6.0.7", "noty": "2.4.1", "reflect-metadata": "0.1.10", diff --git a/communication/pom.xml b/communication/pom.xml index 1e777cf829..344f1d59d5 100644 --- a/communication/pom.xml +++ b/communication/pom.xml @@ -32,5 +32,17 @@ ${entCoreLibsVersion} test + + io.vertx + vertx-codegen + + + + + + maven-compiler-plugin + + + \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java b/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java index 354241d19a..a9172e06f4 100644 --- a/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java +++ b/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java @@ -32,6 +32,7 @@ import fr.wseduc.webutils.http.BaseController; import fr.wseduc.webutils.http.Renders; import fr.wseduc.webutils.request.RequestUtils; +import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpServerRequest; @@ -42,14 +43,32 @@ import org.entcore.common.http.filter.SuperAdminFilter; import org.entcore.common.user.UserUtils; import org.entcore.common.utils.StringUtils; -import org.entcore.common.validation.StringValidation; +import org.entcore.communication.dto.bus.CommunicationBusDTO; +import org.entcore.communication.dto.rest.DiscoverModifyGroupUsersDTO; +import org.entcore.communication.dto.rest.DiscoverVisibleGroupBodyDTO; +import org.entcore.communication.dto.rest.DiscoverVisibleFilterDTO; +import org.entcore.communication.dto.rest.CountResultDTO; +import org.entcore.communication.dto.rest.IdentifiableDTO; +import org.entcore.communication.dto.rest.InitDefaultRulesDTO; +import org.entcore.communication.dto.bus.SearchVisibleBusDTO; +import org.entcore.communication.dto.rest.SearchVisibleRequestDTO; import org.entcore.communication.filters.CommunicationDiscoverVisibleFilter; +import org.entcore.communication.mapper.AddLinkDirectionsDtoMapper; +import org.entcore.communication.mapper.CommuniqueWithDtoMapper; +import org.entcore.communication.mapper.DiscoverVisibleStructureDtoMapper; +import org.entcore.communication.mapper.GroupDtoMapper; +import org.entcore.communication.mapper.RemoveRelationsResultDtoMapper; +import org.entcore.communication.mapper.VerifyResultDtoMapper; +import org.entcore.communication.mapper.SearchVisibleContactDtoMapper; +import org.entcore.communication.mapper.SearchVisibleDtoMapper; +import org.entcore.communication.mapper.UserDtoMapper; import org.entcore.communication.services.CommunicationService; +import org.entcore.communication.mapper.BusUserDtoMapper; import org.entcore.communication.services.impl.DefaultCommunicationService; import java.util.List; +import java.util.stream.Collectors; -import static fr.wseduc.webutils.Utils.isNotEmpty; import static org.entcore.common.http.response.DefaultResponseHandler.*; public class CommunicationController extends BaseController { @@ -64,6 +83,16 @@ public void adminConsole(final HttpServerRequest request) { renderView(request); } + /** + * Adds a communication link between two groups, upgrading the users' communication values + * and propagating the link to all users belonging to the concerned groups. + * + *

Deprecated — do not use. This endpoint was used by the v1 admin console + * and is known to corrupt data. It must not be called anymore.

+ * + * @param request the HTTP request containing path parameters {@code startGroupId} and {@code endGroupId} + * @deprecated This method can corrupt data. Use the new communication API instead. + */ @Post("/group/:startGroupId/communique/:endGroupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() @@ -75,6 +104,15 @@ public void addLink(HttpServerRequest request) { notEmptyResponseHandler(request)); } + /** + * Adds a direct communication link between two users, bypassing group-based rules. + * + *

Restricted to super-admins. Intended for integration testing purposes only + * and should not be used in production workflows.

+ * + * @param request the HTTP request containing path parameters {@code startUser} and {@code endUser}, + * and query parameter {@code direction} defining the direction of the communication link + */ @Put("/api/admin/users/:startUser/communiqueDirect/:endUser") @SecuredAction(value = "", type = ActionType.RESOURCE) @ResourceFilter(SuperAdminFilter.class) @@ -91,6 +129,16 @@ public void setDirectCommunication(HttpServerRequest request) { communicationService.setDirectCommunication(startUser, endUser, directionEnum, defaultResponseHandler(request)); } + /** + * Removes a communication link between two groups, downgrading the users' communication values + * and propagating the removal to all users belonging to the concerned groups. + * + *

Deprecated — do not use. This endpoint was used by the v1 admin console + * and is known to corrupt data. It must not be called anymore.

+ * + * @param request the HTTP request containing path parameters {@code startGroupId} and {@code endGroupId} + * @deprecated This method can corrupt data. Use the new communication API instead. + */ @Delete("/group/:startGroupId/communique/:endGroupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() @@ -102,8 +150,16 @@ public void removeLink(HttpServerRequest request) { notEmptyResponseHandler(request)); } /** - * For test only - * @param request + * Adds a communication link between users and a group, upgrading the group's {@code users} + * value (NONE, INCOMING, OUTGOING, BOTH) and propagating the link to all users belonging + * to the concerned group. + * + *

Deprecated — do not use. This endpoint was used by the v1 admin console + * and is known to corrupt data. It must not be called anymore.

+ * + * @param request the HTTP request containing path parameter {@code groupId} and query parameter + * {@code direction} defining the direction of the communication link + * @deprecated This method can corrupt data. Use the new communication API instead. */ @Post("/group/:groupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @@ -117,8 +173,16 @@ public void addLinksWithUsers(HttpServerRequest request) { } /** - * For test only - * @param request + * Removes a communication link between users and a group, downgrading the group's {@code users} + * value (NONE, INCOMING, OUTGOING, BOTH) and propagating the removal to all users belonging + * to the concerned group. + * + *

Deprecated — do not use. This endpoint was used by the v1 admin console + * and is known to corrupt data. It must not be called anymore.

+ * + * @param request the HTTP request containing path parameter {@code groupId} and query parameter + * {@code direction} defining the direction of the communication link to remove + * @deprecated This method can corrupt data. Use the new communication API instead. */ @Delete("/group/:groupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @@ -131,47 +195,136 @@ public void removeLinksWithUsers(HttpServerRequest request) { communicationService.removeLinkWithUsers(groupId, direction, notEmptyResponseHandler(request)); } + /** + * Returns the group itself along with the list of groups referenced in its {@code communiqueWith} + * Neo4j property, i.e. the groups the source group can communicate with via a {@code COMMUNIQUE} + * relationship. + * + * @param request the HTTP request containing path parameter {@code groupId} + */ @Get("/group/:groupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() public void communiqueWith(HttpServerRequest request) { String groupId = getGroupId(request); if (groupId == null) return; - communicationService.communiqueWith(groupId, notEmptyResponseHandler(request)); + communicationService.communiqueWith(groupId) + .onSuccess(result -> render(request, CommuniqueWithDtoMapper.map(result))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } + /** + * Returns the groups that the given group has outgoing {@code COMMUNIQUE} relations to, + * i.e. groups whose {@code users} value is {@code OUTGOING} or {@code BOTH} and that can + * therefore receive communication and broadcast it to their members. + * + *

Response: array of {@link org.entcore.communication.dto.rest.GroupDTO}.

+ * + * @param request the HTTP request containing path parameter {@code groupId} + */ @Get("/group/:groupId/outgoing") public void getOutgoingRelations(HttpServerRequest request) { String groupId = request.params().get("groupId"); - communicationService.getOutgoingRelations(groupId, arrayResponseHandler(request)); + communicationService.getOutgoingRelations(groupId) + .onSuccess( groups -> render(request, groups.stream().map(JsonObject.class::cast).map(GroupDtoMapper::map).collect(Collectors.toList()))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } + /** + * Returns the groups that have incoming {@code COMMUNIQUE} relations toward the given group, + * i.e. groups whose {@code users} value is {@code INCOMING} or {@code BOTH} and that can + * therefore emit communication from their members. + * + *

Response: array of {@link org.entcore.communication.dto.rest.GroupDTO}.

+ * + * @param request the HTTP request containing path parameter {@code groupId} + */ @Get("/group/:groupId/incoming") public void getIncomingRelations(HttpServerRequest request) { String groupId = request.params().get("groupId"); - communicationService.getIncomingRelations(groupId, arrayResponseHandler(request)); + communicationService.getIncomingRelations(groupId) + .onSuccess( groups -> render(request, groups.stream().map(JsonObject.class::cast).map(GroupDtoMapper::map).collect(Collectors.toList()))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } + /** + * Creates or updates the {@code COMMUNIQUE_DIRECT} relationship between students and their + * relatives for all relative in the given group. Update the relativeCommuniqueStudent group value + * + *

The {@code direction} query parameter controls the direction of the link:

+ * + * + *

Response: {@link org.entcore.communication.dto.rest.CountResultDTO} with the number of + * affected relationships.

+ * + * @param request the HTTP request containing path parameter {@code groupId} and query parameter + * {@code direction} + */ @Post("/relative/:groupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() public void addLinkBetweenRelativeAndStudent(HttpServerRequest request) { String groupId = getGroupId(request); - if (groupId == null) return; + if (groupId == null) { + renderError(request); + return; + } CommunicationService.Direction direction = getDirection(request.params().get("direction")); - communicationService.addLinkBetweenRelativeAndStudent(groupId, direction, notEmptyResponseHandler(request)); + communicationService.addLinkBetweenRelativeAndStudent(groupId, direction) + .onSuccess( count -> render(request, new CountResultDTO(count))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } + /** + * Removes the {@code COMMUNIQUE_DIRECT} relationship between students and their relatives + * for all student/relative pairs in the given group. + * + *

The {@code direction} query parameter controls which link is removed:

+ * + * + *

Response: {@link org.entcore.communication.dto.rest.CountResultDTO} with the number of + * affected relationships.

+ * + * @param request the HTTP request containing path parameter {@code groupId} and query parameter + * {@code direction} + */ @Delete("/relative/:groupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() public void removeLinkBetweenRelativeAndStudent(HttpServerRequest request) { String groupId = getGroupId(request); - if (groupId == null) return; + if (groupId == null) { + renderError(request); + return; + } CommunicationService.Direction direction = getDirection(request.params().get("direction")); - communicationService.removeLinkBetweenRelativeAndStudent(groupId, direction, notEmptyResponseHandler(request)); + communicationService.removeLinkBetweenRelativeAndStudent(groupId, direction) + .onSuccess( count -> render(request, new CountResultDTO(count))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } + /** + * Retrieves all users and groups visible to the given user. + *

+ * Route: {@code GET /visible/:userId} + *

+ * Query parameters: + *

+ * + * @param request the HTTP request; path param {@code userId} identifies the user whose visible contacts are fetched + * @deprecated This endpoint no longer appears to be called. Consider removing it or replacing it with the POST {@code /visible} endpoint. + */ @Get("/visible/:userId") @SecuredAction("communication.visible.user") public void visibleUsers(final HttpServerRequest request) { @@ -185,135 +338,56 @@ public void visibleUsers(final HttpServerRequest request) { } } + /** + * Searches for all users and groups visible to the authenticated user, applying the filters provided in the request body. + *

+ * Route: {@code POST /visible} + *

+ * The request body is deserialized into a {@link SearchVisibleRequestDTO}. The following fields are always + * overridden server-side regardless of client input: + *

+ * Available filters in the body: {@code structures}, {@code classes}, {@code profiles}, {@code functions}, + * {@code positions}, {@code search}, {@code types}, {@code nbUsersInGroups}, {@code groupType}. + *

+ * Returns a {@link org.entcore.communication.dto.rest.SearchVisibleResultDTO} containing: + *

+ * + * @param request the HTTP request carrying the JSON body with search filters + */ @Post("/visible") @SecuredAction(value = "", type = ActionType.AUTHENTICATED) public void searchVisible(HttpServerRequest request) { - RequestUtils.bodyToJson(request, filter -> UserUtils.getUserInfos(eb, request, user -> { - if (user != null) { - String preFilter = ""; - String match = ""; - String where = ""; - String nbUsers = ""; - String groupTypes = ""; - JsonObject params = new JsonObject(); - JsonArray expectedTypes = null; - if (filter != null && filter.size()> 0) { - for (String criteria : filter.fieldNames()) { - switch (criteria) { - case "structures": - case "classes": - JsonArray itemssc = filter.getJsonArray(criteria); - if (itemssc == null || itemssc.isEmpty() || - ("structures".equals(criteria) && filter.getJsonArray("classes") != null && - !filter.getJsonArray("classes").isEmpty())) continue; - if (!params.containsKey("nIds")) { - params.put("nIds", itemssc); - } else { - params.getJsonArray("nIds").addAll(itemssc); - } - if (!match.contains("-[:DEPENDS")) { - if (!match.contains("MATCH ")) { - match = "MATCH "; - where = " WHERE "; - } else { - // We use another MATCH here, because for users not attached to a class, a simple comma does some oddities - match += " MATCH "; - where += "AND "; - } - match += "(visibles)-[:IN*0..1]->()-[:DEPENDS]->(n) "; - where += "n.id IN {nIds} "; - } - if ("structures".equals(criteria)) { - match = match.replaceFirst("\\[:DEPENDS]", "[:DEPENDS*1..2]"); - } - break; - case "profiles": - case "functions": - JsonArray itemspf = filter.getJsonArray(criteria); - if (itemspf == null || itemspf.isEmpty()) continue; - if (!params.containsKey("filters")) { - params.put("filters", itemspf); - } else { - //params.getJsonArray("filters").addAll(itemspf); - params.put("filters2", itemspf); - } - if (!match.contains("MATCH ")) { - match = "MATCH "; - where = " WHERE "; - } else { - // We use another MATCH here, because for users not attached to a class, a simple comma does some oddities - match += " MATCH "; - where += "AND "; - } - if (!match.contains("(visibles)-[:IN*0..1]->(g)")) { - match += "(visibles)-[:IN*0..1]->(g)"; - where += "g.filter IN {filters} "; - } else { - match += "(visibles)-[:IN*0..1]->(g2) "; - where += "g2.filter IN {filters2} "; - } - break; - case "positions": - JsonArray positionIds = filter.getJsonArray(criteria); - if (positionIds == null || positionIds.isEmpty()) continue; - params.put("positionIds", positionIds); - if (!match.contains("MATCH ")) { - where = " WHERE "; - } else { - where += "AND "; - } - where += " ANY(id IN positionIds WHERE id IN {positionIds}) "; - break; - case "search": - final String search = filter.getString(criteria); - if (isNotEmpty(search)) { - preFilter = "AND m.displayNameSearchField CONTAINS {search} "; - - String sanitizedSearch = StringValidation.sanitize(search); - params.put("search", sanitizedSearch); - } - break; - case "types": - JsonArray types = filter.getJsonArray(criteria); - if (types != null && types.size() > 0 && CommunicationService.EXPECTED_TYPES.containsAll(types.getList())) { - expectedTypes = types; - } - break; - case "nbUsersInGroups": - if (filter.getBoolean("nbUsersInGroups", false)) { - nbUsers = ", visibles.nbUsers as nbUsers"; - } - break; - case "groupType": - if (filter.getBoolean("groupType", false)) { - groupTypes = ", labels(visibles) as groupType, visibles.filter as groupProfile"; - } - break; - } - } - } - final boolean returnGroupType = !groupTypes.isEmpty(); - final String customReturn = match + where + - "RETURN DISTINCT visibles.id as id, visibles.name as name, " + - "positionNames, positionIds, " + - "visibles.displayName as displayName, visibles.groupDisplayName as groupDisplayName, " + - "HEAD(visibles.profiles) as profile, subjects" + nbUsers + groupTypes; - communicationService.visibleUsers(user.getUserId(), null, expectedTypes, true, true, false, - preFilter, customReturn, params, user.getType(), false, visibles -> { - if (visibles.isRight()) { - renderJson(request, - UserUtils.translateAndGroupVisible(visibles.right().getValue(), - I18n.acceptLanguage(request), returnGroupType)); - } else { - leftToResponse(request, visibles.left()); - } - }); - } else { - badRequest(request, "invalid.user"); - } + RequestUtils.bodyToClass(request, SearchVisibleRequestDTO.class).onSuccess(rawFilter -> UserUtils.getUserInfos(eb, request, user -> { + SearchVisibleRequestDTO filter = rawFilter.withServerDefaults(); + boolean returnGroupType = Boolean.TRUE.equals(filter.getGroupType()); + communicationService.visibleUsers(user, filter) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))) + .onSuccess( visibles -> { + render(request, SearchVisibleDtoMapper.map(UserUtils.translateAndGroupVisible(visibles, I18n.acceptLanguage(request), returnGroupType))); + }); })); } + /** + * Lists all users in a given group that are visible to the currently authenticated user. + *

+ * Visibility is determined by the communication rules: only users the connected user + * is allowed to communicate with are returned, even if they belong to the group. + * + * @param request the HTTP request; must contain a {@code groupId} path parameter + * identifying the group whose members are to be listed + * @return a list of user objects {@link org.entcore.communication.dto.rest.UserDTO}, + * each containing {@code id}, {@code displayName}, {@code login}, and {@code type} fields, + * ordered by profile type (descending) then display name + */ @Get("/visible/group/:groupId") @SecuredAction(value = "", type = ActionType.AUTHENTICATED) public void visibleGroupContains(HttpServerRequest request) { @@ -324,19 +398,29 @@ public void visibleGroupContains(HttpServerRequest request) { badRequest(request, "invalid.groupId"); return; } + //FIXME we must use a dedicated query to prefilter on users of a group String customReturn = "WITH COLLECT(visibles.id) as vIds " + - "UNWIND vIds AS vId "+ - "MATCH (u:User { id: vId }) "+ - "USING INDEX u:User(id) " + - "MATCH (s:Group { id : {groupId}})<-[:IN]-(u:User) " + - "USING INDEX s:Group(id) "+ - "RETURN DISTINCT HEAD(u.profiles) as type, u.id as id, " + - "u.displayName as displayName, u.login as login " + - "ORDER BY type DESC, displayName "; + "UNWIND vIds AS vId "+ + "MATCH (u:User { id: vId }) "+ + "USING INDEX u:User(id) " + + "MATCH (s:Group { id : {groupId}})<-[:IN]-(u:User) " + + "USING INDEX s:Group(id) "+ + "RETURN DISTINCT HEAD(u.profiles) as type, u.id as id, " + + "u.displayName as displayName, u.login as login " + + "ORDER BY type DESC, displayName "; final JsonObject params = new JsonObject().put("groupId", groupId); communicationService.visibleUsers(user.getUserId(), null, null, true, true, false, null, - customReturn, params, arrayResponseHandler(request)); + customReturn, params, visibles -> { + if(visibles.isLeft()) { + renderError(request, new JsonObject().put("error", visibles.left())); + return; + } + render(request, visibles.right().getValue().stream() + .map(JsonObject.class::cast) + .map(UserDtoMapper::map) + .collect(Collectors.toList())); + }); } else { badRequest(request, "invalid.user"); } @@ -345,65 +429,56 @@ public void visibleGroupContains(HttpServerRequest request) { @BusAddress("wse.communication.users") public void visibleUsers(final Message message) { - String userId = message.body().getString("userId"); - if (userId != null && !userId.trim().isEmpty()) { - String action = message.body().getString("action", ""); - String schoolId = message.body().getString("schoolId"); - JsonArray expectedTypes = message.body().getJsonArray("expectedTypes"); - Handler> responseHandler = new Handler>() { - - @Override - public void handle(Either res) { - JsonArray j; - if (res.isRight()) { - j = res.right().getValue(); - } else { - log.warn(res.left().getValue()); - j = new JsonArray(); - } - message.reply(j); - } - }; - switch (action) { + SearchVisibleBusDTO searchQuery = new SearchVisibleBusDTO(message.body()); + String userId = searchQuery.getUserId(); + if (userId == null || userId.trim().isEmpty()) { + message.reply(new JsonArray()); + return; + } + + String schoolId = searchQuery.getSchoolId(); + JsonArray expectedTypes = new JsonArray(searchQuery.getExpectedTypes()); + final Future future; + + switch (searchQuery.getAction()) { case "visibleUsers": - String preFilter = message.body().getString("preFilter"); - String customReturn = message.body().getString("customReturn"); - JsonObject ap = message.body().getJsonObject("additionnalParams"); - boolean itSelf = message.body().getBoolean("itself", false); - boolean myGroup = communicationService instanceof DefaultCommunicationService ? true : - message.body().getBoolean("mygroup", false); - boolean profile = message.body().getBoolean("profile", true); - String userProfile = message.body().getString("userProfile", null); - boolean reverseUnion = message.body().getBoolean("reverseUnion", false); - communicationService.visibleUsers(userId, schoolId, expectedTypes, itSelf, myGroup, - profile, preFilter, customReturn, ap, userProfile, reverseUnion, responseHandler); + boolean myGroup = communicationService instanceof DefaultCommunicationService || searchQuery.isMyGroup(); + //FIXME: we can't define a DTO until we remove customeReturn from query + future = communicationService.visibleUsers(userId, schoolId, expectedTypes, + searchQuery.isItSelf(), myGroup, searchQuery.isProfile(), + searchQuery.getPreFilter(), searchQuery.getCustomReturn(), + new JsonObject(searchQuery.getAdditionnalParams()), + searchQuery.getUserProfile(), searchQuery.isReverseUnion()); break; case "visibleUsersForShare": - String search = message.body().getString("search"); - JsonArray userIds = message.body().getJsonArray("userIds"); - communicationService.visibleUsersForShare(userId, search, userIds, responseHandler); - break; + future = communicationService.visibleUsersForShare(userId, searchQuery.getSearch(), + new JsonArray(searchQuery.getUserIds())) + .map(arr -> serializeForBus(arr, BusUserDtoMapper::map)); + break; case "usersCanSeeMe": - communicationService.usersCanSeeMe(userId, responseHandler); + future = communicationService.usersCanSeeMe(userId) + .map(arr -> serializeForBus(arr, BusUserDtoMapper::map)); break; case "visibleProfilsGroups": - String pF = message.body().getString("preFilter"); - String c = message.body().getString("customReturn"); - JsonObject p = message.body().getJsonObject("additionnalParams"); - communicationService.visibleProfilsGroups(userId, c, p, pF, responseHandler); + //FIXME: we can't define a DTO until we remove customeReturn from query + future = communicationService.visibleProfilsGroups(userId, searchQuery.getCustomReturn(), + new JsonObject(searchQuery.getAdditionnalParams()), searchQuery.getPreFilter()); break; case "visibleManualGroups": - String cr = message.body().getString("customReturn"); - JsonObject pa = message.body().getJsonObject("additionnalParams"); - communicationService.visibleManualGroups(userId, cr, pa, responseHandler); + //FIXME: we can't define a DTO until we remove customeReturn from query + future = communicationService.visibleManualGroups(userId, searchQuery.getCustomReturn(), + new JsonObject(searchQuery.getAdditionnalParams())); break; default: message.reply(new JsonArray()); - break; - } - } else { - message.reply(new JsonArray()); + return; } + + future.onSuccess(message::reply) + .onFailure(err -> { + log.warn(err.getMessage()); + message.reply(new JsonArray()); + }); } private void visibleUsers(String userId, String schoolId, JsonArray expectedTypes, @@ -417,24 +492,38 @@ private void visibleUsers(String userId, String schoolId, JsonArray expectedType userId, schoolId, expectedTypes, itSelf, false, true, null, customReturn, additionnalParams, handler); } + /** + * Initializes default communication rules for the specified structures. + *

+ * Rules are read from the {@code initDefaultCommunicationRules} entry of the module configuration. + * + *

Response: {@code 200 OK} with an empty JSON object {@code {}} on success. + * + * @param request the HTTP request containing a {@link InitDefaultRulesDTO} JSON body + */ @Put("/init/rules") @SecuredAction("communication.init.default.rules") public void initDefaultCommunicationRules(final HttpServerRequest request) { - RequestUtils.bodyToJson(request, new Handler() { - @Override - public void handle(JsonObject body) { - JsonObject initDefaultRules = config.getJsonObject("initDefaultCommunicationRules"); - JsonArray structures = body.getJsonArray("structures"); - if (structures != null && structures.size() > 0) { - communicationService.initDefaultRules(structures, - initDefaultRules, defaultResponseHandler(request)); - } else { - badRequest(request, "invalid.structures"); - } + RequestUtils.bodyToClass(request, InitDefaultRulesDTO.class).onSuccess(body -> { + if (body.getStructures() == null || body.getStructures().isEmpty()) { + badRequest(request, "invalid.structures"); + return; } - }); + JsonObject initDefaultRules = config.getJsonObject("initDefaultCommunicationRules"); + communicationService.initDefaultRules(new JsonArray(body.getStructures()), initDefaultRules) + .onSuccess(result -> renderJson(request, result)) + .onFailure(err -> renderError(request, new JsonObject().put("error", err.getMessage()))); + }).onFailure(err -> badRequest(request, err.getMessage())); } + /** + * Resets the communication rules of a structure to their default values. + *

+ * Restricted to super-admins. + * + * @param request the HTTP request; must contain a {@code structureId} path parameter + * identifying the structure whose rules are to be reset + */ @Post("/rules/:structureId/reset") @SecuredAction(value = "", type = ActionType.RESOURCE) @ResourceFilter(SuperAdminFilter.class) @@ -457,6 +546,16 @@ public void getDefaultCommunicationRules(final HttpServerRequest request) { Renders.renderJson(request, initDefaultRules, 200); } + /** + * Applies the default communication rules to a structure. + *

+ * Only adds the rules defined as defaults — rules that already exist but are not part of + * the defaults are left untouched and will not be removed. + * To fully reset a structure's rules to defaults, use {@link #resetRules} instead. + * + * @param request the HTTP request; must contain a {@code structureId} path parameter + * identifying the structure to apply the default rules to + */ @Put("/rules/:structureId") @SecuredAction("communication.default.rules") public void defaultCommunicationRules(final HttpServerRequest request) { @@ -471,110 +570,92 @@ public void defaultCommunicationRules(final HttpServerRequest request) { @BusAddress("wse.communication") public void communicationEventBusHandler(final Message message) { + CommunicationBusDTO dto = new CommunicationBusDTO(message.body()); JsonObject initDefaultRules = config.getJsonObject("initDefaultCommunicationRules"); - final Handler> responseHandler = new Handler>() { + final Future future; - @Override - public void handle(Either res) { - if (res.isRight()) { - message.reply(res.right().getValue()); - } else { - message.reply(new JsonObject().put("status", "error") - .put("message", res.left().getValue())); - } - } - }; - switch (message.body().getString("action", "")) { - case "initDefaultCommunicationRules" : - communicationService.initDefaultRules(message.body().getJsonArray("schoolIds"), - initDefaultRules, responseHandler); + switch (dto.getAction() != null ? dto.getAction() : "") { + case "initDefaultCommunicationRules": + future = communicationService.initDefaultRules(new JsonArray(dto.getSchoolIds()), initDefaultRules); break; - case "initAndApplyDefaultCommunicationRules" : - final Integer transactionId = message.body().getInteger("transactionId"); - final Boolean commit = message.body().getBoolean("commit", true); - communicationService.initDefaultRules(message.body().getJsonArray("schoolIds"), - initDefaultRules, transactionId, commit, new Handler>() { - @Override - public void handle(Either event) { - if (event.isRight()) { - communicationService.applyDefaultRules(message.body().getJsonArray("schoolIds"), - transactionId, commit, responseHandler); - } else { - message.reply(new JsonObject().put("status", "error") - .put("message", event.left().getValue())); - } - } - }); + case "initAndApplyDefaultCommunicationRules": + future = communicationService.initDefaultRules(new JsonArray(dto.getSchoolIds()), initDefaultRules, + dto.getTransactionId(), dto.isCommit()) + .compose(ignored -> communicationService.applyDefaultRules(new JsonArray(dto.getSchoolIds()), + dto.getTransactionId(), dto.isCommit())); break; - case "setDefaultCommunicationRules" : - communicationService.applyDefaultRules(new JsonArray().add( - message.body().getString("schoolId")), responseHandler); + case "setDefaultCommunicationRules": + future = communicationService.applyDefaultRules(new JsonArray().add(dto.getSchoolId())); break; - case "setMultipleDefaultCommunicationRules" : - communicationService.applyDefaultRules( - message.body().getJsonArray("schoolIds"), responseHandler); + case "setMultipleDefaultCommunicationRules": + future = communicationService.applyDefaultRules(new JsonArray(dto.getSchoolIds())); break; - case "setCommunicationRules" : - communicationService.applyRules( - message.body().getString("groupId"), responseHandler); + case "setCommunicationRules": + future = communicationService.applyRules(dto.getGroupId()); break; - case "addLink" : - // Create communication link between two groups - if (message.body().containsKey("startGroupId") && message.body().containsKey("endGroupId")) { - String startGroupId = message.body().getString("startGroupId"); - String endGroupId = message.body().getString("endGroupId"); - - communicationService.addLink(startGroupId, endGroupId, responseHandler); + case "addLink": + if (dto.getStartGroupId() == null || dto.getEndGroupId() == null) { + future = Future.failedFuture("missing.parameters"); } else { - message.reply(new JsonObject().put("status", "error") - .put("message", "missing.parameters")); + future = communicationService.addLink(dto.getStartGroupId(), dto.getEndGroupId()); } break; - case "addLinkWithUsers" : - if (message.body().containsKey("groupId") && message.body().containsKey("direction")) { - String groupId = message.body().getString("groupId"); - String direction = message.body().getString("direction", "BOTH"); - CommunicationService.Direction directionEnum; - try { - directionEnum = CommunicationService.Direction.valueOf(direction.toUpperCase()); - } catch (IllegalArgumentException | NullPointerException e) { - directionEnum = CommunicationService.Direction.BOTH; - } - - communicationService.addLinkWithUsers(groupId, directionEnum, responseHandler); - }else { - message.reply(new JsonObject().put("status", "error") - .put("message", "missing.parameters")); + case "addLinkWithUsers": + if (dto.getGroupId() == null || dto.getDirection() == null) { + future = Future.failedFuture("missing.parameters"); + } else { + future = communicationService.addLinkWithUsers(dto.getGroupId(), parseDirection(dto.getDirection())); } break; - case "removeLinkWithUsers" : - if (message.body().containsKey("groupId") && message.body().containsKey("direction")) { - String groupId = message.body().getString("groupId"); - String direction = message.body().getString("direction", "BOTH"); - CommunicationService.Direction directionEnum; - try { - directionEnum = CommunicationService.Direction.valueOf(direction.toUpperCase()); - } catch (IllegalArgumentException | NullPointerException e) { - directionEnum = CommunicationService.Direction.BOTH; - } - - communicationService.removeLinkWithUsers(groupId, directionEnum, responseHandler); + case "removeLinkWithUsers": + if (dto.getGroupId() == null || dto.getDirection() == null) { + future = Future.failedFuture("missing.parameters"); } else { - message.reply(new JsonObject().put("status", "error") - .put("message", "missing.parameters")); + future = communicationService.removeLinkWithUsers(dto.getGroupId(), parseDirection(dto.getDirection())); } break; default: - message.reply(new JsonObject().put("status", "error") - .put("message", "invalid.action")); + future = Future.failedFuture("invalid.action"); + } + + future + .onSuccess(message::reply) + .onFailure(err -> message.reply(new JsonObject().put("status", "error").put("message", err.getMessage()))); + } + + private static CommunicationService.Direction parseDirection(String direction) { + try { + return CommunicationService.Direction.valueOf(direction.toUpperCase()); + } catch (IllegalArgumentException | NullPointerException e) { + return CommunicationService.Direction.BOTH; } } + /** + * Removes all communication rules of profile groups belonging to the given structure, + * including direct ({@code communiqueDirect}) links between users of those profile groups. + *

+ * Other group types (manual groups, etc.) and inbound links from groups outside the structure + * pointing to the structure's profile groups are not affected. + * + *

Deprecated — do not use. Because only a subset of rules and links is + * removed, the {@code communiqueWith} and {@code users} values on the affected Neo4j group nodes + * are not recalculated after the deletion. This leaves those values stale and can lead to data + * corruption. Use {@link #resetRules} to safely reset a structure's rules to their default state. + * + * @param request the HTTP request; must contain a {@code structureId} query parameter + * identifying the structure whose profile-group rules are to be removed + * @deprecated Can corrupt {@code communiqueWith} / {@code users} values in Neo4j. + * Use {@link #resetRules} instead. + */ + @Deprecated @Delete("/rules") @SecuredAction("communication.remove.rules") public void removeCommunicationRules(HttpServerRequest request) { String structureId = request.params().get("structureId"); - communicationService.removeRules(structureId, defaultResponseHandler(request)); + communicationService.removeRules(structureId) + .onSuccess(v -> Renders.ok(request)) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } public void setCommunicationService(CommunicationService communicationService) { @@ -636,42 +717,76 @@ private String getGroupId(HttpServerRequest request) { return groupId; } + /** + * Sets the communication direction of a group to {@code BOTH} and adds the corresponding + * communication links between the group and its users if they do not already exist. + *

+ * Used by the v2 admin console to grant two-way communication between a group and its members. + * + * @param request the HTTP request; must contain a {@code groupId} path parameter + * identifying the group to update + * @return {@code 200 OK} with a {@link GroupUsersDirectionDTO} body confirming the applied direction + */ @Post("/group/:groupId/users") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() public void safelyAddLinksWithUsers(HttpServerRequest request) { String groupId = getGroupId(request); if (groupId == null) return; - communicationService.addLinkWithUsers(groupId, CommunicationService.Direction.BOTH, event -> { - if (event.isRight()) { - Renders.renderJson(request, new JsonObject().put("users", CommunicationService.Direction.BOTH), 200); - } else { - JsonObject error = new JsonObject().put("error", event.left().getValue()); - Renders.renderJson(request, error, 400); - } - }); + communicationService.addLinkWithUsers(groupId, CommunicationService.Direction.BOTH) + .onSuccess(v -> Renders.renderJson(request, new JsonObject().put("users", CommunicationService.Direction.BOTH), 200)) + .onFailure(th -> renderJson(request, new JsonObject().put("error", th.getMessage()), 400)); } + /** + * Safely downgrades the communication direction of a group by removing its link with users, + * if doing so is consistent with the group's existing inbound and outbound relations. + *

+ * The service counts how many other groups send to ({@code numberOfSendingGroups}) and receive + * from ({@code numberOfReceivingGroups}) this group, then computes the minimum direction still + * required. For example, a group whose {@code users} value is {@code BOTH} but only receives + * communication from other groups can be safely downgraded to {@code OUTGOING} + * (meaning the group receives communication and dispatches it to its members). + *

+ * If no safe downgrade exists, the operation is refused. + * + * @param request the HTTP request; must contain a {@code groupId} path parameter + * identifying the group to downgrade + * @return {@code 200 OK} with a {@link GroupUsersDirectionDTO} body containing the resulting direction, + * {@code 409 Conflict} if the direction cannot be safely changed, + * or {@code 500} on unexpected error + */ @Delete("/group/:groupId/users") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() public void safelyRemoveLinksWithUsers(HttpServerRequest request) { String groupId = getGroupId(request); if (groupId == null) return; - communicationService.safelyRemoveLinkWithUsers(groupId, event -> { - if (event.isLeft()) { - String error = event.left().getValue(); - if (error.equals(CommunicationService.IMPOSSIBLE_TO_CHANGE_DIRECTION)) { - Renders.renderJson(request, new JsonObject().put("error", error), 409); - } else { - Renders.renderJson(request, new JsonObject().put("error", error), 500); - } - } else { - Renders.renderJson(request, event.right().getValue(), 200); - } - }); + communicationService.safelyRemoveLinkWithUsers(groupId) + .onSuccess(result -> Renders.renderJson(request, result, 200)) + .onFailure(th -> { + int status = CommunicationService.IMPOSSIBLE_TO_CHANGE_DIRECTION.equals(th.getMessage()) ? 409 : 500; + renderJson(request, new JsonObject().put("error", th.getMessage()), status); + }); } + /** + * Checks whether a communication link from {@code startGroupId} to {@code endGroupId} can be + * added by the authenticated user, without actually creating the link. + *

+ * The check takes into account the current {@code users} direction of both groups and the + * caller's role. For example, an ADML cannot add a link {@code g1 -> g2} if {@code g2} is + * currently {@code INCOMING}, because doing so would require upgrading {@code g2} to + * {@code BOTH} — an operation restricted to super-admins for certain group types. + * + * @param request the HTTP request; must contain path parameters {@code startGroupId} and + * {@code endGroupId} identifying the source and target groups + * @return {@code 200 OK} with a JSON body {@code {"warning": ""}} where {@code warning} + * is a non-null string if the link is possible but carries a notable side-effect, + * or {@code null} if the link can be added without any caveat; + * {@code 409 Conflict} if the link cannot be added at all given the caller's permissions, + * or {@code 500} on unexpected error + */ @Get("/v2/group/:startGroupId/communique/:endGroupId/check") @ResourceFilter(AdminFilter.class) @SecuredAction(value = "", type = ActionType.RESOURCE) @@ -680,22 +795,29 @@ public void addLinkCheckOnly(HttpServerRequest request) { Params params = new Params(request).validate(); if (params.isInvalid()) return; - UserUtils.getUserInfos(eb, request, user -> { - communicationService.addLinkCheckOnly(params.getStartGroupId(), params.getEndGroupId(), user, event -> { - if (event.isLeft()) { - String error = event.left().getValue(); - if (CommunicationService.IMPOSSIBLE_TO_CHANGE_DIRECTION.equals(error)) { - Renders.renderJson(request, new JsonObject().put("error", error), 409); - } else { - Renders.renderJson(request, new JsonObject().put("error", error), 500); - } - } else { - Renders.renderJson(request, event.right().getValue(), 200); - } - }); - }); + UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.addLinkCheckOnly(params.getStartGroupId(), params.getEndGroupId(), user)) + .onSuccess(result -> Renders.renderJson(request, result, 200)) + .onFailure(th -> { + int status = CommunicationService.IMPOSSIBLE_TO_CHANGE_DIRECTION.equals(th.getMessage()) ? 409 : 500; + renderJson(request, new JsonObject().put("error", th.getMessage()), status); + }); } + /** + * Adds a communication link from {@code startGroupId} to {@code endGroupId} and adjusts the + * {@code users} direction of each group if necessary to reflect the new relation. + *

+ * The operation is refused if adding the link would require a direction upgrade that the + * authenticated user is not permitted to perform (e.g. an ADML upgrading a restricted group + * to {@code BOTH}). Use {@link #addLinkCheckOnly} first to verify feasibility without side effects. + * + * @param request the HTTP request; must contain path parameters {@code startGroupId} and + * {@code endGroupId} identifying the source and target groups + * @return {@code 200 OK} with an {@link AddLinkDirectionsDTO} body containing the resulting + * direction of each group after the link was applied, + * or {@code 500} if the operation failed or was not permitted + */ @Post("/v2/group/:startGroupId/communique/:endGroupId") @ResourceFilter(AdminFilter.class) @SecuredAction(value = "", type = ActionType.RESOURCE) @@ -704,28 +826,34 @@ public void processAddLinkAndChangeDirection(HttpServerRequest request) { Params params = new Params(request).validate(); if (params.isInvalid()) return; - communicationService.addLink(params.getStartGroupId(), params.getEndGroupId(), event -> { - if (event.isLeft()) { - Renders.renderJson(request, new JsonObject().put("error", event.left().getValue()), 500); - } else { - communicationService.processChangeDirectionAfterAddingLink(params.getStartGroupId(), params.getEndGroupId(), eventChange -> { - if (event.isLeft()) { - Renders.renderJson(request, new JsonObject().put("error", event.left().getValue()), 500); - } else { - Renders.renderJson(request, eventChange.right().getValue(), 200); - } - }); - } - }); + communicationService.addLink(params.getStartGroupId(), params.getEndGroupId()) + .compose(v -> communicationService.processChangeDirectionAfterAddingLink(params.getStartGroupId(), params.getEndGroupId())) + .onSuccess(result -> render(request, AddLinkDirectionsDtoMapper.map(result))) + .onFailure(th -> renderJson(request, new JsonObject().put("error", th.getMessage()), 500)); } + /** + * Removes the communication relation between two groups and recalculates the minimal + * {@code users} direction required for each group based on their remaining communication links. + *

+ * After the link is removed, the service inspects all still-existing inbound and outbound + * relations of both groups and downgrades their direction to the lowest value that still + * satisfies those remaining relations. + * + * @param request the HTTP request; must contain path parameters {@code startGroupId} and + * {@code endGroupId} identifying the sender and receiver groups + * @return {@code 200 OK} with a {@link RemoveRelationsResultDTO} body containing the + * recalculated direction of the sender and the receiver after the removal + */ @Delete("/group/:startGroupId/relations/:endGroupId") @SecuredAction(value = "", type = ActionType.RESOURCE) @MfaProtected() public void removeRelations(HttpServerRequest request) { Params params = new Params(request).validate(); if (params.isInvalid()) return; - communicationService.removeRelations(params.getStartGroupId(), params.getEndGroupId(), notEmptyResponseHandler(request)); + communicationService.removeRelations(params.getStartGroupId(), params.getEndGroupId()) + .onSuccess(result -> render(request, RemoveRelationsResultDtoMapper.map(result))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** @@ -750,70 +878,87 @@ public void verify(HttpServerRequest request) { badRequest(request, "invalid.parameter"); return; } - communicationService.verify(from, to, notEmptyResponseHandler(request)); + communicationService.verify(from, to) + .onSuccess(result -> render(request, VerifyResultDtoMapper.map(result))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Discover visible users, return list of users that can be dicover by the user - * @param request - * { - * "structures": ["id1", "id2"], - * "profiles": ["Teacher"], - * "search": "search", - * } + * Returns the list of users discoverable by the authenticated user, optionally filtered by + * structures, profiles, and a search string. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. * - * */ + * @param request the HTTP request containing a {@link DiscoverVisibleFilterDTO} JSON body + * @return {@code 200 OK} with a JSON array of {@link UserDTO} entries + */ @Post("/discover/visible/users") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) public void getDiscoverVisibleUsers(HttpServerRequest request) { - RequestUtils.bodyToJson(request, filter -> UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.getDiscoverVisibleUsers( user.getUserId(), filter, arrayResponseHandler(request)); - })); + RequestUtils.bodyToClass(request, DiscoverVisibleFilterDTO.class) + .compose(filter -> UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.getDiscoverVisibleUsers(user.getUserId(), filter))) + .onSuccess(results -> render(request, results.stream() + .map(JsonObject.class::cast) + .map(UserDtoMapper::mapDiscoverVisible) + .collect(Collectors.toList()))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Return list of accepted profile for discover visible - * */ + * Returns the list of profiles accepted by the discover-network feature for the authenticated user. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request + * @return {@code 200 OK} with a JSON array of profile name strings (e.g. {@code ["Teacher", "Student"]}) + */ @Get("/discover/visible/profiles") @SecuredAction(value= "", type = ActionType.AUTHENTICATED) public void getDiscoverVisibleAcceptedProfile(HttpServerRequest request) { - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.getDiscoverVisibleAcceptedProfile(user, arrayResponseHandler(request)); - }); + UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.getDiscoverVisibleAcceptedProfile(user)) + .onSuccess(results -> render(request, results.getList())) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Return list of all structures, to be use to filter discover visible users - * */ + * Returns the list of all structures available as filters for the discover-network feature. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request + * @return {@code 200 OK} with a JSON array of {@link DiscoverVisibleStructureDTO} entries + */ @Get("/discover/visible/structures") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) public void getDiscoverVisibleStructures(HttpServerRequest request) { - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.getDiscoverVisibleStructures(arrayResponseHandler(request)); - }); + UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.getDiscoverVisibleStructures()) + .onSuccess(results -> render(request, results.stream() + .map(JsonObject.class::cast) + .map(DiscoverVisibleStructureDtoMapper::map) + .collect(Collectors.toList()))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Add communication rights between two users, this methode use "COMMUIQUE_DIRECT" with source 'MANUAL' - * @param request - * { - * "receiverId": "receiverId" - * } - * */ + * Creates a direct communication link ({@code COMMUNIQUE_DIRECT} with source {@code MANUAL}) + * between the authenticated user and the specified receiver. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request; must contain a {@code receiverId} path parameter + * identifying the user to establish communication with + * @return {@code 200 OK} with a {@link CountResultDTO} body indicating the number of + * communication links created + */ @Post("/discover/visible/add/commuting/:receiverId") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) @@ -823,19 +968,24 @@ public void discoverVisibleAddCommuteUsers(HttpServerRequest request) { badRequest(request, "invalid.parameter"); return; } - - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.discoverVisibleAddCommuteUsers(user, receiverId, request, notEmptyResponseHandler(request)); - }); + UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.discoverVisibleAddCommuteUsers(user, receiverId, request)) + .onSuccess(result -> render(request, new CountResultDTO(result.getInteger("number")))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Delete communication rights between two users, this methode delete "COMMUIQUE_DIRECT" with source 'MANUAL' - * */ + * Removes the direct communication link ({@code COMMUNIQUE_DIRECT} with source {@code MANUAL}) + * between the authenticated user and the specified receiver. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request; must contain a {@code receiverId} path parameter + * identifying the user whose communication link should be removed + * @return {@code 200 OK} with a {@link CountResultDTO} body indicating the number of + * communication links removed + */ @Delete("/discover/visible/remove/commuting/:receiverId") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) @@ -845,39 +995,44 @@ public void discoverVisibleRemoveCommuteUsers(HttpServerRequest request) { badRequest(request, "invalid.parameter"); return; } - - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.discoverVisibleRemoveCommuteUsers(user.getUserId(), receiverId, notEmptyResponseHandler(request)); - }); + UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.discoverVisibleRemoveCommuteUsers(user.getUserId(), receiverId)) + .onSuccess(result -> render(request, new CountResultDTO(result.getInteger("number")))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * This methode return all groups that the user can see, with the group type 'manager' - * */ + * Returns all groups of type {@code manager} visible to the authenticated user. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request + * @return {@code 200 OK} with a JSON array of {@link GroupDTO} entries + */ @Get("/discover/visible/groups") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) public void discoverVisibleGetGroups(HttpServerRequest request) { - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.discoverVisibleGetGroups(user.getUserId(), arrayResponseHandler(request)); - }); + UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.discoverVisibleGetGroups(user.getUserId())) + .onSuccess(results -> render(request, results.stream() + .map(JsonObject.class::cast) + .map(GroupDtoMapper::map) + .collect(Collectors.toList()))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Get the list of users in a group - * @param request - * { - * "groupId": "groupId" - * } - * */ + * Returns the list of users belonging to a given discover-network group, filtered to those + * visible to the authenticated user. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request; must contain a {@code groupId} path parameter + * @return {@code 200 OK} with a JSON array of {@link UserDTO} entries + */ @Get("/discover/visible/group/:groupId/users") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) @@ -885,46 +1040,52 @@ public void discoverVisibleGetUsersInGroup(HttpServerRequest request) { String groupId = request.params().get("groupId"); - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.discoverVisibleGetUsersInGroup(user.getUserId(), groupId, arrayResponseHandler(request)); - }); + UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.discoverVisibleGetUsersInGroup(user.getUserId(), groupId)) + .onSuccess(results -> render(request, results.stream() + .map(JsonObject.class::cast) + .map(UserDtoMapper::mapDiscoverVisibleGroupUser) + .collect(Collectors.toList()))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Create a group with the group type 'manager' - * @param request - * { - * "name": "name", - * } - * */ + * Creates a group of type {@code manager} for the discover-network feature. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request containing a {@link DiscoverVisibleGroupBodyDTO} JSON body + * @return {@code 200 OK} with an {@link IdentifiableDTO} body containing the id of the + * newly created group + */ @Post("/discover/visible/group") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) public void createDiscoverVisibleGroup(HttpServerRequest request) { - RequestUtils.bodyToJson(request, body -> { - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.createDiscoverVisibleGroup(user.getUserId(), body, notEmptyResponseHandler(request)); - }); - }); + RequestUtils.bodyToClass(request, DiscoverVisibleGroupBodyDTO.class).compose(body -> { + if (body.getName() == null || body.getName().trim().isEmpty()) { + return Future.failedFuture("invalid.name"); + } + return UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.createDiscoverVisibleGroup(user.getUserId(), body)); + }) + .onSuccess(result -> render(request, new IdentifiableDTO(result.getString("id"), null))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Update a group with the group type 'manager' - * @param request - * { - * "groupId": "groupId", - * "name": "name", - * } - * */ + * Updates the name of a discover-network group of type {@code manager}. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request; must contain a {@code groupId} path parameter and a + * {@link DiscoverVisibleGroupBodyDTO} JSON body with the new name + * @return {@code 200 OK} with an {@link IdentifiableDTO} body containing the id of the + * updated group + */ @Put("/discover/visible/group/:groupId") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) @@ -934,26 +1095,30 @@ public void updateDiscoverVisibleGroup(HttpServerRequest request) { badRequest(request, "invalid.parameter"); return; } - RequestUtils.bodyToJson(request, body -> { - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.updateDiscoverVisibleGroup(user.getUserId(), groupId, body, notEmptyResponseHandler(request)); - }); - }); + RequestUtils.bodyToClass(request, DiscoverVisibleGroupBodyDTO.class) + .compose(body -> { + if (body.getName() == null || body.getName().trim().isEmpty()) { + return Future.failedFuture("invalid.name"); + } + return UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.updateDiscoverVisibleGroup(user.getUserId(), groupId, body)); + }).onSuccess(result -> render(request, new IdentifiableDTO(result.getString("id"), null))) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** - * Update the groups member, with adding the new user in the group and send a notification to the user, and removing the user from the group - * @param request - * { - * "groupId": "groupId", - * "oldUsers": ["userId1", "userId2"], // list of users befor change - * "newUsers": "["userId1", "userId2"]" //list of users after change - * } - * */ + * Updates the members of a discover-network group by diffing the old and new user lists: + * users added to the group receive a notification, users removed from the group are silently + * unlinked. + *

+ * Normandie only — this endpoint is part of the discover-network feature + * and is only active in the Normandie deployment. + * + * @param request the HTTP request; must contain a {@code groupId} path parameter and a + * {@link DiscoverModifyGroupUsersDTO} JSON body with the previous and new + * member lists (max 100 users in {@code newUsers}) + * @return {@code 200 OK} on success + */ @Put("/discover/visible/group/:groupId/users") @SecuredAction(value= "", type = ActionType.RESOURCE) @ResourceFilter(CommunicationDiscoverVisibleFilter.class) @@ -964,15 +1129,18 @@ public void addDiscoverVisibleGroupUsers(HttpServerRequest request) { return; } - RequestUtils.bodyToJson(request, body -> { - UserUtils.getUserInfos(eb, request, user -> { - if(user == null) { - badRequest(request, "invalid.user"); - return; - } - communicationService.addDiscoverVisibleGroupUsers(user, groupId, body, request, defaultResponseHandler(request)); - }); - }); + RequestUtils.bodyToClass(request, DiscoverModifyGroupUsersDTO.class).compose(body -> { + if (body.getNewUsers() == null || body.getNewUsers().isEmpty()) { + return Future.failedFuture("invalid.users"); + } + if (body.getNewUsers().size() > 100) { + return Future.failedFuture("too.many.users"); + } + return UserUtils.getAuthenticatedUserInfos(eb, request) + .compose(user -> communicationService.addDiscoverVisibleGroupUsers(user, groupId, body, request)); + }) + .onSuccess(v -> Renders.ok(request)) + .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); } /** @@ -991,7 +1159,10 @@ public void searchVisibleContacts(HttpServerRequest request) { final String mode = request.params().get("mode"); final boolean includeHidden = "true".equalsIgnoreCase(request.params().get("includeHidden")); communicationService.searchVisibles(userInfos, query, mode, I18n.acceptLanguage(request), includeHidden) - .onSuccess(visibles -> renderJson(request, visibles)) + .onSuccess(visibles -> render(request, visibles.stream() + .map(JsonObject.class::cast) + .map(SearchVisibleContactDtoMapper::map) + .collect(Collectors.toList()))) .onFailure(th -> renderError(request, new JsonObject().put("error", th.getMessage()))); }); } diff --git a/communication/src/main/java/org/entcore/communication/dto/bus/BusUserDTO.java b/communication/src/main/java/org/entcore/communication/dto/bus/BusUserDTO.java new file mode 100644 index 0000000000..740c5abdc0 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/bus/BusUserDTO.java @@ -0,0 +1,72 @@ +package org.entcore.communication.dto.bus; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@DataObject +@JsonGen +public class BusUserDTO { + + private String id; + private String name; + private String login; + private String username; + private String type; + private String profile; + private String lastName; + private String firstName; + private List profiles; + private List positionIds; + private List positionNames; + + public BusUserDTO() {} + + public BusUserDTO(JsonObject json) { + this(); + BusUserDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + BusUserDTOConverter.toJson(this, json); + return json; + } + + public String getId() { return id; } + public BusUserDTO setId(String id) { this.id = id; return this; } + + public String getName() { return name; } + public BusUserDTO setName(String name) { this.name = name; return this; } + + public String getLogin() { return login; } + public BusUserDTO setLogin(String login) { this.login = login; return this; } + + public String getUsername() { return username; } + public BusUserDTO setUsername(String username) { this.username = username; return this; } + + public String getType() { return type; } + public BusUserDTO setType(String type) { this.type = type; return this; } + + public String getProfile() { return profile; } + public BusUserDTO setProfile(String profile) { this.profile = profile; return this; } + + public String getLastName() { return lastName; } + public BusUserDTO setLastName(String lastName) { this.lastName = lastName; return this; } + + public String getFirstName() { return firstName; } + public BusUserDTO setFirstName(String firstName) { this.firstName = firstName; return this; } + + public List getProfiles() { return profiles; } + public BusUserDTO setProfiles(List profiles) { this.profiles = profiles; return this; } + + public List getPositionIds() { return positionIds; } + public BusUserDTO setPositionIds(List positionIds) { this.positionIds = positionIds; return this; } + + public List getPositionNames() { return positionNames; } + public BusUserDTO setPositionNames(List positionNames) { this.positionNames = positionNames; return this; } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/bus/CommunicationBusDTO.java b/communication/src/main/java/org/entcore/communication/dto/bus/CommunicationBusDTO.java new file mode 100644 index 0000000000..f38d22c09e --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/bus/CommunicationBusDTO.java @@ -0,0 +1,62 @@ +package org.entcore.communication.dto.bus; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; +import java.util.List; + +@DataObject +@JsonGen +public class CommunicationBusDTO { + + private String action; + private List schoolIds; + private String schoolId; + private Integer transactionId; + private Boolean commit; + private String groupId; + private String startGroupId; + private String endGroupId; + private String direction; + + public CommunicationBusDTO() {} + + public CommunicationBusDTO(JsonObject json) { + this(); + CommunicationBusDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + CommunicationBusDTOConverter.toJson(this, json); + return json; + } + + public String getAction() { return action; } + public CommunicationBusDTO setAction(String action) { this.action = action; return this; } + + public List getSchoolIds() { return schoolIds; } + public CommunicationBusDTO setSchoolIds(List schoolIds) { this.schoolIds = schoolIds; return this; } + + public String getSchoolId() { return schoolId; } + public CommunicationBusDTO setSchoolId(String schoolId) { this.schoolId = schoolId; return this; } + + public Integer getTransactionId() { return transactionId; } + public CommunicationBusDTO setTransactionId(Integer transactionId) { this.transactionId = transactionId; return this; } + + /** Defaults to {@code true} when absent from the JSON payload. */ + public boolean isCommit() { return commit == null || commit; } + public CommunicationBusDTO setCommit(boolean commit) { this.commit = commit; return this; } + + public String getGroupId() { return groupId; } + public CommunicationBusDTO setGroupId(String groupId) { this.groupId = groupId; return this; } + + public String getStartGroupId() { return startGroupId; } + public CommunicationBusDTO setStartGroupId(String startGroupId) { this.startGroupId = startGroupId; return this; } + + public String getEndGroupId() { return endGroupId; } + public CommunicationBusDTO setEndGroupId(String endGroupId) { this.endGroupId = endGroupId; return this; } + + public String getDirection() { return direction; } + public CommunicationBusDTO setDirection(String direction) { this.direction = direction; return this; } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/bus/SearchVisibleBusDTO.java b/communication/src/main/java/org/entcore/communication/dto/bus/SearchVisibleBusDTO.java new file mode 100644 index 0000000000..d1b4e46cf4 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/bus/SearchVisibleBusDTO.java @@ -0,0 +1,88 @@ +package org.entcore.communication.dto.bus; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@DataObject +@JsonGen +public class SearchVisibleBusDTO { + + private String userId; + private List userIds; + private String action; + private String schoolId; + private List expectedTypes = new ArrayList<>(); + private String userProfile; + private String preFilter; + private String customReturn; + private boolean reverseUnion; + private Map additionnalParams; + // fields from the former SearchVisibleRequestDTO parent + private String search; + private boolean itSelf; + private boolean myGroup; + private boolean profile = true; + + public SearchVisibleBusDTO() {} + + public SearchVisibleBusDTO(JsonObject json) { + this(); + SearchVisibleBusDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + SearchVisibleBusDTOConverter.toJson(this, json); + return json; + } + + public String getUserId() { return userId; } + public SearchVisibleBusDTO setUserId(String userId) { this.userId = userId; return this; } + + public List getUserIds() { return userIds; } + public SearchVisibleBusDTO setUserIds(List userIds) { this.userIds = userIds; return this; } + + public String getAction() { return action; } + public SearchVisibleBusDTO setAction(String action) { this.action = action; return this; } + + public String getSchoolId() { return schoolId; } + public SearchVisibleBusDTO setSchoolId(String schoolId) { this.schoolId = schoolId; return this; } + + public List getExpectedTypes() { return expectedTypes; } + public SearchVisibleBusDTO setExpectedTypes(List expectedTypes) { + this.expectedTypes = expectedTypes != null ? expectedTypes : new ArrayList<>(); + return this; + } + + public String getUserProfile() { return userProfile; } + public SearchVisibleBusDTO setUserProfile(String userProfile) { this.userProfile = userProfile; return this; } + + public String getPreFilter() { return preFilter; } + public SearchVisibleBusDTO setPreFilter(String preFilter) { this.preFilter = preFilter; return this; } + + public String getCustomReturn() { return customReturn; } + public SearchVisibleBusDTO setCustomReturn(String customReturn) { this.customReturn = customReturn; return this; } + + public boolean isReverseUnion() { return reverseUnion; } + public SearchVisibleBusDTO setReverseUnion(boolean reverseUnion) { this.reverseUnion = reverseUnion; return this; } + + public Map getAdditionnalParams() { return additionnalParams; } + public SearchVisibleBusDTO setAdditionnalParams(Map additionnalParams) { this.additionnalParams = additionnalParams; return this; } + + public String getSearch() { return search; } + public SearchVisibleBusDTO setSearch(String search) { this.search = search; return this; } + + public boolean isItSelf() { return itSelf; } + public SearchVisibleBusDTO setItSelf(boolean itSelf) { this.itSelf = itSelf; return this; } + + public boolean isMyGroup() { return myGroup; } + public SearchVisibleBusDTO setMyGroup(boolean myGroup) { this.myGroup = myGroup; return this; } + + /** Defaults to {@code true} when absent from the JSON payload. */ + public boolean isProfile() { return profile; } + public SearchVisibleBusDTO setProfile(boolean profile) { this.profile = profile; return this; } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/bus/package-info.java b/communication/src/main/java/org/entcore/communication/dto/bus/package-info.java new file mode 100644 index 0000000000..d2cbd0612c --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/bus/package-info.java @@ -0,0 +1,4 @@ +@ModuleGen(name = "communication-dto-bus", groupPackage = "org.entcore.communication") +package org.entcore.communication.dto.bus; + +import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/AddLinkDirectionsDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/AddLinkDirectionsDTO.java new file mode 100644 index 0000000000..93940f32f3 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/AddLinkDirectionsDTO.java @@ -0,0 +1,18 @@ +package org.entcore.communication.dto.rest; + +import org.entcore.communication.services.CommunicationService; + +import java.util.Map; + +public class AddLinkDirectionsDTO { + + private final Map directions; + + public AddLinkDirectionsDTO(Map directions) { + this.directions = directions; + } + + public Map getDirections() { + return directions; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/CommuniqueWithDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/CommuniqueWithDTO.java new file mode 100644 index 0000000000..337df766a7 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/CommuniqueWithDTO.java @@ -0,0 +1,38 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@DataObject +@JsonGen(inheritConverter = true) +public class CommuniqueWithDTO extends GroupDTO { + + private List communiqueWith; + + public CommuniqueWithDTO() {} + + public CommuniqueWithDTO(JsonObject json) { + this(); + CommuniqueWithDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + CommuniqueWithDTOConverter.toJson(this, json); + return json; + } + + public List getCommuniqueWith() { + return communiqueWith; + } + + public CommuniqueWithDTO setCommuniqueWith(List communiqueWith) { + this.communiqueWith = communiqueWith; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/ContactRefDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/ContactRefDTO.java new file mode 100644 index 0000000000..49ff195135 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/ContactRefDTO.java @@ -0,0 +1,46 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@DataObject +@JsonGen +public class ContactRefDTO { + + private String id; + private String displayName; + + public ContactRefDTO() {} + + public ContactRefDTO(JsonObject json) { + this(); + ContactRefDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + ContactRefDTOConverter.toJson(this, json); + return json; + } + + public String getId() { + return id; + } + + public ContactRefDTO setId(String id) { + this.id = id; + return this; + } + + public String getDisplayName() { + return displayName; + } + + public ContactRefDTO setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/CountResultDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/CountResultDTO.java new file mode 100644 index 0000000000..efa7e9a97f --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/CountResultDTO.java @@ -0,0 +1,14 @@ +package org.entcore.communication.dto.rest; + +public class CountResultDTO { + + private final int count; + + public CountResultDTO(int count) { + this.count = count; + } + + public int getCount() { + return count; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverModifyGroupUsersDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverModifyGroupUsersDTO.java new file mode 100644 index 0000000000..c79f5602f4 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverModifyGroupUsersDTO.java @@ -0,0 +1,48 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; +import java.util.ArrayList; +import java.util.List; + +@DataObject +@JsonGen +@JsonIgnoreProperties(ignoreUnknown = true) +public class DiscoverModifyGroupUsersDTO { + + private List oldUsers = new ArrayList<>(); + private List newUsers; + + public DiscoverModifyGroupUsersDTO() {} + + public DiscoverModifyGroupUsersDTO(JsonObject json) { + this(); + DiscoverModifyGroupUsersDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + DiscoverModifyGroupUsersDTOConverter.toJson(this, json); + return json; + } + + public List getOldUsers() { + return oldUsers; + } + + public DiscoverModifyGroupUsersDTO setOldUsers(List oldUsers) { + this.oldUsers = oldUsers != null ? oldUsers : new ArrayList<>(); + return this; + } + + public List getNewUsers() { + return newUsers; + } + + public DiscoverModifyGroupUsersDTO setNewUsers(List newUsers) { + this.newUsers = newUsers; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleFilterDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleFilterDTO.java new file mode 100644 index 0000000000..e5df403e0b --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleFilterDTO.java @@ -0,0 +1,57 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; +import java.util.List; + +@DataObject +@JsonGen +@JsonIgnoreProperties(ignoreUnknown = true) +public class DiscoverVisibleFilterDTO { + + private List structures; + private List profiles; + private String search; + + public DiscoverVisibleFilterDTO() {} + + public DiscoverVisibleFilterDTO(JsonObject json) { + this(); + DiscoverVisibleFilterDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + DiscoverVisibleFilterDTOConverter.toJson(this, json); + return json; + } + + public List getStructures() { + return structures; + } + + public DiscoverVisibleFilterDTO setStructures(List structures) { + this.structures = structures; + return this; + } + + public List getProfiles() { + return profiles; + } + + public DiscoverVisibleFilterDTO setProfiles(List profiles) { + this.profiles = profiles; + return this; + } + + public String getSearch() { + return search; + } + + public DiscoverVisibleFilterDTO setSearch(String search) { + this.search = search; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleGroupBodyDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleGroupBodyDTO.java new file mode 100644 index 0000000000..752d8441b0 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleGroupBodyDTO.java @@ -0,0 +1,36 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +@DataObject +@JsonGen +@JsonIgnoreProperties(ignoreUnknown = true) +public class DiscoverVisibleGroupBodyDTO { + + private String name; + + public DiscoverVisibleGroupBodyDTO() {} + + public DiscoverVisibleGroupBodyDTO(JsonObject json) { + this(); + DiscoverVisibleGroupBodyDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + DiscoverVisibleGroupBodyDTOConverter.toJson(this, json); + return json; + } + + public String getName() { + return name; + } + + public DiscoverVisibleGroupBodyDTO setName(String name) { + this.name = name; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleStructureDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleStructureDTO.java new file mode 100644 index 0000000000..9d45c50ac6 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/DiscoverVisibleStructureDTO.java @@ -0,0 +1,24 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DiscoverVisibleStructureDTO { + + private final String id; + private final String type; + private final String label; + private final boolean checked; + + public DiscoverVisibleStructureDTO(String id, String type, String label, boolean checked) { + this.id = id; + this.type = type; + this.label = label; + this.checked = checked; + } + + public String getId() { return id; } + public String getType() { return type; } + public String getLabel() { return label; } + public boolean isChecked() { return checked; } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/GroupDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/GroupDTO.java new file mode 100644 index 0000000000..a86914160e --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/GroupDTO.java @@ -0,0 +1,156 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +import java.util.ArrayList; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@DataObject +@JsonGen(inheritConverter = true) +public class GroupDTO extends IdentifiableDTO { + + private String filter; + private String profile; + private String displayName; + private String groupDisplayName; + private InternalCommunicationRule internalCommunicationRule; + private List classes = new ArrayList<>(); + private List structures = new ArrayList<>(); + private String type; + private String groupType; + private String subType; + private String sortName; + private Integer nbUsers; + + public GroupDTO() {} + + public GroupDTO(JsonObject json) { + this(); + GroupDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + GroupDTOConverter.toJson(this, json); + return json; + } + + public String getFilter() { + return filter; + } + + public GroupDTO setFilter(String filter) { + this.filter = filter; + return this; + } + + public String getDisplayName() { + return displayName; + } + + public GroupDTO setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + public InternalCommunicationRule getInternalCommunicationRule() { + return internalCommunicationRule; + } + + public GroupDTO setInternalCommunicationRule(InternalCommunicationRule internalCommunicationRule) { + this.internalCommunicationRule = internalCommunicationRule; + return this; + } + + public List getClasses() { + return classes; + } + + public GroupDTO setClasses(List classes) { + this.classes = classes; + return this; + } + + public List getStructures() { + return structures; + } + + public GroupDTO setStructures(List structures) { + this.structures = structures; + return this; + } + + public String getSubType() { + return subType; + } + + public GroupDTO setSubType(String subType) { + this.subType = subType; + return this; + } + + public String getType() { + return type; + } + + public GroupDTO setType(String type) { + this.type = type; + return this; + } + + public String getProfile() { + return profile; + } + + public GroupDTO setProfile(String profile) { + this.profile = profile; + return this; + } + + public String getGroupType() { + return groupType; + } + + public GroupDTO setGroupType(String groupType) { + this.groupType = groupType; + return this; + } + + public String getSortName() { + return sortName; + } + + public GroupDTO setSortName(String sortName) { + this.sortName = sortName; + return this; + } + + public Integer getNbUsers() { + return nbUsers; + } + + public GroupDTO setNbUsers(Integer nbUsers) { + this.nbUsers = nbUsers; + return this; + } + + public String getGroupDisplayName() { + return groupDisplayName; + } + + public GroupDTO setGroupDisplayName(String groupDisplayName) { + this.groupDisplayName = groupDisplayName; + return this; + } + + public enum InternalCommunicationRule { + NONE, + INCOMING, + OUTGOING, + BOTH; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/GroupUsersDirectionDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/GroupUsersDirectionDTO.java new file mode 100644 index 0000000000..9050a89028 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/GroupUsersDirectionDTO.java @@ -0,0 +1,16 @@ +package org.entcore.communication.dto.rest; + +import org.entcore.communication.services.CommunicationService; + +public class GroupUsersDirectionDTO { + + private final CommunicationService.Direction users; + + public GroupUsersDirectionDTO(CommunicationService.Direction users) { + this.users = users; + } + + public CommunicationService.Direction getUsers() { + return users; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/IdentifiableDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/IdentifiableDTO.java new file mode 100644 index 0000000000..1c577e2d21 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/IdentifiableDTO.java @@ -0,0 +1,51 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@DataObject +@JsonGen +public class IdentifiableDTO { + + private String id; + private String name; + + public IdentifiableDTO() {} + + public IdentifiableDTO(String id, String name) { + this.id = id; + this.name = name; + } + + public IdentifiableDTO(JsonObject json) { + this(); + IdentifiableDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + IdentifiableDTOConverter.toJson(this, json); + return json; + } + + public String getId() { + return id; + } + + public IdentifiableDTO setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public IdentifiableDTO setName(String name) { + this.name = name; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/InitDefaultRulesDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/InitDefaultRulesDTO.java new file mode 100644 index 0000000000..4f349742e9 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/InitDefaultRulesDTO.java @@ -0,0 +1,35 @@ +package org.entcore.communication.dto.rest; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; +import java.util.List; + +@DataObject +@JsonGen +public class InitDefaultRulesDTO { + + private List structures; + + public InitDefaultRulesDTO() {} + + public InitDefaultRulesDTO(JsonObject json) { + this(); + InitDefaultRulesDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + InitDefaultRulesDTOConverter.toJson(this, json); + return json; + } + + public List getStructures() { + return structures; + } + + public InitDefaultRulesDTO setStructures(List structures) { + this.structures = structures; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/RemoveRelationsResultDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/RemoveRelationsResultDTO.java new file mode 100644 index 0000000000..00077a18bd --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/RemoveRelationsResultDTO.java @@ -0,0 +1,22 @@ +package org.entcore.communication.dto.rest; + +import org.entcore.communication.services.CommunicationService; + +public class RemoveRelationsResultDTO { + + private final CommunicationService.Direction sender; + private final CommunicationService.Direction receiver; + + public RemoveRelationsResultDTO(CommunicationService.Direction sender, CommunicationService.Direction receiver) { + this.sender = sender; + this.receiver = receiver; + } + + public CommunicationService.Direction getSender() { + return sender; + } + + public CommunicationService.Direction getReceiver() { + return receiver; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleContactDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleContactDTO.java new file mode 100644 index 0000000000..ed86a0a9b3 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleContactDTO.java @@ -0,0 +1,134 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@DataObject +@JsonGen +public class SearchVisibleContactDTO { + + private String id; + private String displayName; + private String type; + private List usedIn; + + // users and ProfileGroup + private String profile; + + // groups + private String groupType; + private Integer nbUsers; + private String structureName; + + // non-student users + private List children; + private List relatives; + + public SearchVisibleContactDTO() {} + + public SearchVisibleContactDTO(JsonObject json) { + this(); + SearchVisibleContactDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + SearchVisibleContactDTOConverter.toJson(this, json); + return json; + } + + public String getId() { + return id; + } + + public SearchVisibleContactDTO setId(String id) { + this.id = id; + return this; + } + + public String getDisplayName() { + return displayName; + } + + public SearchVisibleContactDTO setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + public String getType() { + return type; + } + + public SearchVisibleContactDTO setType(String type) { + this.type = type; + return this; + } + + public List getUsedIn() { + return usedIn; + } + + public SearchVisibleContactDTO setUsedIn(List usedIn) { + this.usedIn = usedIn; + return this; + } + + public String getProfile() { + return profile; + } + + public SearchVisibleContactDTO setProfile(String profile) { + this.profile = profile; + return this; + } + + public String getGroupType() { + return groupType; + } + + public SearchVisibleContactDTO setGroupType(String groupType) { + this.groupType = groupType; + return this; + } + + public Integer getNbUsers() { + return nbUsers; + } + + public SearchVisibleContactDTO setNbUsers(Integer nbUsers) { + this.nbUsers = nbUsers; + return this; + } + + public String getStructureName() { + return structureName; + } + + public SearchVisibleContactDTO setStructureName(String structureName) { + this.structureName = structureName; + return this; + } + + public List getChildren() { + return children; + } + + public SearchVisibleContactDTO setChildren(List children) { + this.children = children; + return this; + } + + public List getRelatives() { + return relatives; + } + + public SearchVisibleContactDTO setRelatives(List relatives) { + this.relatives = relatives; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleRequestDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleRequestDTO.java new file mode 100644 index 0000000000..b922844653 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleRequestDTO.java @@ -0,0 +1,84 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; +import java.util.List; + +@DataObject +@JsonGen +@JsonIgnoreProperties(ignoreUnknown = true) +public class SearchVisibleRequestDTO { + + private List structures; + private List classes; + private List profiles; + private List functions; + private List positions; + private String search; + private List types; + private Boolean nbUsersInGroups; + private Boolean groupType; + private boolean itSelf; + private boolean myGroup; + private boolean profile = true; + + public SearchVisibleRequestDTO() {} + + public SearchVisibleRequestDTO(JsonObject json) { + this(); + SearchVisibleRequestDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + SearchVisibleRequestDTOConverter.toJson(this, json); + return json; + } + + /** + * Returns this instance after applying server-side overrides for the REST visible endpoint: + * {@code itSelf=true}, {@code myGroup=true}, {@code profile=false}. + */ + public SearchVisibleRequestDTO withServerDefaults() { + return setItSelf(true).setMyGroup(true).setProfile(false); + } + + public List getStructures() { return structures; } + public SearchVisibleRequestDTO setStructures(List structures) { this.structures = structures; return this; } + + public List getClasses() { return classes; } + public SearchVisibleRequestDTO setClasses(List classes) { this.classes = classes; return this; } + + public List getProfiles() { return profiles; } + public SearchVisibleRequestDTO setProfiles(List profiles) { this.profiles = profiles; return this; } + + public List getFunctions() { return functions; } + public SearchVisibleRequestDTO setFunctions(List functions) { this.functions = functions; return this; } + + public List getPositions() { return positions; } + public SearchVisibleRequestDTO setPositions(List positions) { this.positions = positions; return this; } + + public String getSearch() { return search; } + public SearchVisibleRequestDTO setSearch(String search) { this.search = search; return this; } + + public List getTypes() { return types; } + public SearchVisibleRequestDTO setTypes(List types) { this.types = types; return this; } + + public Boolean getNbUsersInGroups() { return nbUsersInGroups; } + public SearchVisibleRequestDTO setNbUsersInGroups(Boolean nbUsersInGroups) { this.nbUsersInGroups = nbUsersInGroups; return this; } + + public Boolean getGroupType() { return groupType; } + public SearchVisibleRequestDTO setGroupType(Boolean groupType) { this.groupType = groupType; return this; } + + public boolean isItSelf() { return itSelf; } + public SearchVisibleRequestDTO setItSelf(boolean itSelf) { this.itSelf = itSelf; return this; } + + public boolean isMyGroup() { return myGroup; } + public SearchVisibleRequestDTO setMyGroup(boolean myGroup) { this.myGroup = myGroup; return this; } + + /** Defaults to {@code true} when absent from the JSON payload. */ + public boolean isProfile() { return profile; } + public SearchVisibleRequestDTO setProfile(boolean profile) { this.profile = profile; return this; } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleResultDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleResultDTO.java new file mode 100644 index 0000000000..b4670b5fae --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/SearchVisibleResultDTO.java @@ -0,0 +1,22 @@ +package org.entcore.communication.dto.rest; + +import java.util.List; + +public class SearchVisibleResultDTO { + + private final List groups; + private final List users; + + public SearchVisibleResultDTO(List groups, List users) { + this.groups = groups; + this.users = users; + } + + public List getGroups() { + return groups; + } + + public List getUsers() { + return users; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/UserDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/UserDTO.java new file mode 100644 index 0000000000..d3154e92c7 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/UserDTO.java @@ -0,0 +1,100 @@ +package org.entcore.communication.dto.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class UserDTO extends IdentifiableDTO { + + private String displayName; + private String groupDisplayName; + private String profile; + private String type; + private String login; + private List subjects; + private List positions; + private List structures; + private Boolean hasCommunication; + + public String getDisplayName() { + return displayName; + } + + public UserDTO setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + public String getGroupDisplayName() { + return groupDisplayName; + } + + public UserDTO setGroupDisplayName(String groupDisplayName) { + this.groupDisplayName = groupDisplayName; + return this; + } + + public String getProfile() { + return profile; + } + + public UserDTO setProfile(String profile) { + this.profile = profile; + return this; + } + + public List getSubjects() { + return subjects; + } + + public UserDTO setSubjects(List subjects) { + this.subjects = subjects; + return this; + } + + public List getPositions() { + return positions; + } + + public UserDTO setPositions(List positions) { + this.positions = positions; + return this; + } + + public String getType() { + return type; + } + + public UserDTO setType(String type) { + this.type = type; + return this; + } + + public String getLogin() { + return login; + } + + public UserDTO setLogin(String login) { + this.login = login; + return this; + } + + public List getStructures() { + return structures; + } + + public UserDTO setStructures(List structures) { + this.structures = structures; + return this; + } + + public Boolean getHasCommunication() { + return hasCommunication; + } + + public UserDTO setHasCommunication(Boolean hasCommunication) { + this.hasCommunication = hasCommunication; + return this; + } +} diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/VerifyResultDTO.java b/communication/src/main/java/org/entcore/communication/dto/rest/VerifyResultDTO.java new file mode 100644 index 0000000000..5cbd7dd0fb --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/VerifyResultDTO.java @@ -0,0 +1,38 @@ +package org.entcore.communication.dto.rest; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +@DataObject +@JsonGen +public class VerifyResultDTO { + + private boolean canCommunicate; + + public VerifyResultDTO() {} + + public VerifyResultDTO(boolean canCommunicate) { + this.canCommunicate = canCommunicate; + } + + public VerifyResultDTO(JsonObject json) { + this(); + VerifyResultDTOConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + VerifyResultDTOConverter.toJson(this, json); + return json; + } + + public boolean isCanCommunicate() { + return canCommunicate; + } + + public VerifyResultDTO setCanCommunicate(boolean canCommunicate) { + this.canCommunicate = canCommunicate; + return this; + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/dto/rest/package-info.java b/communication/src/main/java/org/entcore/communication/dto/rest/package-info.java new file mode 100644 index 0000000000..b6bac9a276 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/dto/rest/package-info.java @@ -0,0 +1,4 @@ +@ModuleGen(name = "communication-dto-rest", groupPackage = "org.entcore.communication") +package org.entcore.communication.dto.rest; + +import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/AddLinkDirectionsDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/AddLinkDirectionsDtoMapper.java new file mode 100644 index 0000000000..e47d3304ad --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/AddLinkDirectionsDtoMapper.java @@ -0,0 +1,20 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.AddLinkDirectionsDTO; +import org.entcore.communication.services.CommunicationService; + +import java.util.HashMap; +import java.util.Map; + +public class AddLinkDirectionsDtoMapper { + + public static AddLinkDirectionsDTO map(JsonObject json) { + Map directions = new HashMap<>(); + if (!json.containsKey("ok")) { + json.forEach(entry -> directions.put(entry.getKey(), + CommunicationService.Direction.fromString(entry.getValue().toString()))); + } + return new AddLinkDirectionsDTO(directions); + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/BusUserDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/BusUserDtoMapper.java new file mode 100644 index 0000000000..71e14ff617 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/BusUserDtoMapper.java @@ -0,0 +1,11 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.bus.BusUserDTO; + +public class BusUserDtoMapper { + + public static BusUserDTO map(JsonObject json) { + return new BusUserDTO(json); + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/CommuniqueWithDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/CommuniqueWithDtoMapper.java new file mode 100644 index 0000000000..dc754ed9aa --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/CommuniqueWithDtoMapper.java @@ -0,0 +1,11 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.CommuniqueWithDTO; + +public class CommuniqueWithDtoMapper { + + public static CommuniqueWithDTO map(JsonObject json) { + return json == null ? null : new CommuniqueWithDTO(json); + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/DiscoverVisibleStructureDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/DiscoverVisibleStructureDtoMapper.java new file mode 100644 index 0000000000..c6066ede3b --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/DiscoverVisibleStructureDtoMapper.java @@ -0,0 +1,16 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.DiscoverVisibleStructureDTO; + +public class DiscoverVisibleStructureDtoMapper { + + public static DiscoverVisibleStructureDTO map(JsonObject json) { + return new DiscoverVisibleStructureDTO( + json.getString("id"), + json.getString("type"), + json.getString("label"), + Boolean.parseBoolean(json.getString("checked")) + ); + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/GroupDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/GroupDtoMapper.java new file mode 100644 index 0000000000..a95150970f --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/GroupDtoMapper.java @@ -0,0 +1,12 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.GroupDTO; + +public class GroupDtoMapper { + + public static GroupDTO map(JsonObject json) { + return json == null ? null : new GroupDTO(json); + } + +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/IdentifiableDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/IdentifiableDtoMapper.java new file mode 100644 index 0000000000..72ea47592c --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/IdentifiableDtoMapper.java @@ -0,0 +1,12 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.IdentifiableDTO; + +public class IdentifiableDtoMapper { + + public static IdentifiableDTO map(JsonObject json) { + return new IdentifiableDTO(json); + } + +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/RemoveRelationsResultDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/RemoveRelationsResultDtoMapper.java new file mode 100644 index 0000000000..a1fda91255 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/RemoveRelationsResultDtoMapper.java @@ -0,0 +1,17 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.RemoveRelationsResultDTO; +import org.entcore.communication.services.CommunicationService; + +public class RemoveRelationsResultDtoMapper { + + public static RemoveRelationsResultDTO map(JsonObject json) { + String sender = json.getString("sender"); + String receiver = json.getString("receiver"); + return new RemoveRelationsResultDTO( + sender != null ? CommunicationService.Direction.fromString(sender) : null, + receiver != null ? CommunicationService.Direction.fromString(receiver) : null + ); + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/SearchVisibleContactDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/SearchVisibleContactDtoMapper.java new file mode 100644 index 0000000000..679f80f55c --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/SearchVisibleContactDtoMapper.java @@ -0,0 +1,11 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.SearchVisibleContactDTO; + +public class SearchVisibleContactDtoMapper { + + public static SearchVisibleContactDTO map(JsonObject json) { + return new SearchVisibleContactDTO(json); + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/SearchVisibleDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/SearchVisibleDtoMapper.java new file mode 100644 index 0000000000..ebc243e7e5 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/SearchVisibleDtoMapper.java @@ -0,0 +1,27 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.GroupDTO; +import org.entcore.communication.dto.rest.SearchVisibleResultDTO; +import org.entcore.communication.dto.rest.UserDTO; + +import java.util.List; +import java.util.stream.Collectors; + +public class SearchVisibleDtoMapper { + + public static SearchVisibleResultDTO map(JsonObject json) { + List groups = json.getJsonArray("groups").stream() + .map(JsonObject.class::cast) + .map(GroupDtoMapper::map) + .collect(Collectors.toList()); + + List users = json.getJsonArray("users").stream() + .map(JsonObject.class::cast) + .map(UserDtoMapper::map) + .collect(Collectors.toList()); + + return new SearchVisibleResultDTO(groups, users); + } + +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/mapper/UserDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/UserDtoMapper.java new file mode 100644 index 0000000000..7f4180ab66 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/UserDtoMapper.java @@ -0,0 +1,55 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.IdentifiableDTO; +import org.entcore.communication.dto.rest.UserDTO; + +import java.util.stream.Collectors; + +public class UserDtoMapper { + + public static UserDTO mapDiscoverVisible(JsonObject json) { + UserDTO dto = new UserDTO(); + dto.setId(json.getString("id")) + .setName(json.getString("name")); + dto.setDisplayName(json.getString("displayName")) + .setGroupDisplayName(json.getString("groupDisplayName")) + .setProfile(json.getString("profile")) + .setHasCommunication(json.getBoolean("hasCommunication")); + if (json.getJsonArray("structures") != null) { + dto.setStructures(json.getJsonArray("structures").getList()); + } + return dto; + } + + public static UserDTO mapDiscoverVisibleGroupUser(JsonObject json) { + UserDTO dto = new UserDTO(); + dto.setId(json.getString("id")); + dto.setDisplayName(json.getString("displayName")) + .setType(json.getString("type")) + .setLogin(json.getString("login")) + .setHasCommunication(json.getBoolean("hasCommunication")); + return dto; + } + + public static UserDTO map(JsonObject user) { + UserDTO dto = new UserDTO(); + dto.setId(user.getString("id")); + dto.setName(user.getString("name")); + dto.setDisplayName(user.getString("displayName")) + .setProfile(user.getString("profile")); + if (user.getJsonArray("subjects") != null) { + dto.setSubjects( user.getJsonArray("subjects").getList()); + } + if(user.getJsonArray("positions") != null) { + dto.setPositions(user.getJsonArray("positions").stream() + .map(JsonObject.class::cast) + .map( o -> new IdentifiableDTO(o.getString("id"), o.getString("name"))) + .collect(Collectors.toList())); + } + dto.setType(user.getString("type")) + .setLogin(user.getString("login")); + return dto; + } + +} diff --git a/communication/src/main/java/org/entcore/communication/mapper/VerifyResultDtoMapper.java b/communication/src/main/java/org/entcore/communication/mapper/VerifyResultDtoMapper.java new file mode 100644 index 0000000000..68284291f8 --- /dev/null +++ b/communication/src/main/java/org/entcore/communication/mapper/VerifyResultDtoMapper.java @@ -0,0 +1,11 @@ +package org.entcore.communication.mapper; + +import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.VerifyResultDTO; + +public class VerifyResultDtoMapper { + + public static VerifyResultDTO map(JsonObject json) { + return new VerifyResultDTO(json); + } +} \ No newline at end of file diff --git a/communication/src/main/java/org/entcore/communication/services/CommunicationService.java b/communication/src/main/java/org/entcore/communication/services/CommunicationService.java index c2057889ca..2d0a0d9a93 100644 --- a/communication/src/main/java/org/entcore/communication/services/CommunicationService.java +++ b/communication/src/main/java/org/entcore/communication/services/CommunicationService.java @@ -31,6 +31,10 @@ import io.vertx.core.http.HttpServerRequest; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import org.entcore.communication.dto.rest.DiscoverModifyGroupUsersDTO; +import org.entcore.communication.dto.rest.DiscoverVisibleGroupBodyDTO; +import org.entcore.communication.dto.rest.DiscoverVisibleFilterDTO; +import org.entcore.communication.dto.rest.SearchVisibleRequestDTO; public interface CommunicationService { String IMPOSSIBLE_TO_CHANGE_DIRECTION = "impossible to change direction"; @@ -98,41 +102,58 @@ static public Direction fromBitmask(int bitmask) { void addLink(String startGroupId, String endGroupId, Handler> handler); + Future addLink(String startGroupId, String endGroupId); + void removeLink(String startGroupId, String endGroupId, Handler> handler); void addLinkWithUsers(String groupId, Direction direction, Handler> handler); + Future addLinkWithUsers(String groupId, Direction direction); + void addLinkWithUsers(Map params, Handler> handler); void removeLinkWithUsers(String groupId, Direction direction, Handler> handler); + Future removeLinkWithUsers(String groupId, Direction direction); + void communiqueWith(String groupId,// VisibleType filter, Handler> handler); - void addLinkBetweenRelativeAndStudent(String groupId, Direction direction, - Handler> handler); + Future communiqueWith(String groupId); - void removeLinkBetweenRelativeAndStudent(String groupId, Direction direction, - Handler> handler); + Future addLinkBetweenRelativeAndStudent(String groupId, Direction direction); + + Future removeLinkBetweenRelativeAndStudent(String groupId, Direction direction); void initDefaultRules(JsonArray structureIds, JsonObject defaultRules, final Integer transactionId, final Boolean commit, final Handler> handler); + Future initDefaultRules(JsonArray structureIds, JsonObject defaultRules, Integer transactionId, + Boolean commit); + void initDefaultRules(JsonArray structureIds, JsonObject defaultRules, Handler> handler); + Future initDefaultRules(JsonArray structureIds, JsonObject defaultRules); + void applyDefaultRules(JsonArray structureIds, final Integer transactionId, final Boolean commit, Handler> handler); + Future applyDefaultRules(JsonArray structureIds, Integer transactionId, Boolean commit); + void applyDefaultRules(JsonArray structureIds, Handler> handler); + Future applyDefaultRules(JsonArray structureIds); + void applyRules(String groupId, Handler> responseHandler); - void removeRules(String structureId, Handler> handler); + Future applyRules(String groupId); + + Future removeRules(String structureId); void visibleUsers(String userId, String structureId, JsonArray expectedTypes, boolean itSelf, boolean myGroup, boolean profile, String preFilter, String customReturn, JsonObject additionalParams, @@ -143,27 +164,42 @@ void visibleUsers(String userId, String structureId, JsonArray expectedTypes, bo boolean reverseUnion, Handler> handler); + Future visibleUsers(String userId, String structureId, JsonArray expectedTypes, boolean itSelf, + boolean myGroup, boolean profile, String preFilter, String customReturn, JsonObject additionalParams, + String userProfile, boolean reverseUnion); + + Future visibleUsers(UserInfos userInfos, SearchVisibleRequestDTO searchVisibleDto); + void usersCanSeeMe(String userId, final Handler> handler); + Future usersCanSeeMe(String userId); + void visibleProfilsGroups(String userId, String customReturn, JsonObject additionnalParams, String preFilter, Handler> handler); + Future visibleProfilsGroups(String userId, String customReturn, JsonObject additionnalParams, + String preFilter); + void visibleManualGroups(String userId, String customReturn, JsonObject additionnalParams, Handler> handler); - void getOutgoingRelations(String id, Handler> results); + Future visibleManualGroups(String userId, String customReturn, JsonObject additionnalParams); + + Future visibleUsersForShare(String userId, String search, JsonArray userIds); + + Future getOutgoingRelations(String id); - void getIncomingRelations(String id, Handler> results); + Future getIncomingRelations(String id); - void safelyRemoveLinkWithUsers(String groupId, Handler> handler); + Future safelyRemoveLinkWithUsers(String groupId); void getDirections(String startGroupId, String endGroupId, Handler> handler); - void addLinkCheckOnly(String startGroupId, String endGroupId, UserInfos userInfos, Handler> handler); + Future addLinkCheckOnly(String startGroupId, String endGroupId, UserInfos userInfos); - void processChangeDirectionAfterAddingLink(String startGroupId, String endGroupId, Handler> handler); + Future processChangeDirectionAfterAddingLink(String startGroupId, String endGroupId); - void removeRelations(String startGroupId, String endGroupId, Handler> handler); + Future removeRelations(String startGroupId, String endGroupId); /** * Indicates if a sender (user) can communicate to a receiver (user or group) on using @@ -176,27 +212,27 @@ void visibleManualGroups(String userId, String customReturn, JsonObject addition * @param recipientId id of the recipient * @param handler final handler */ - void verify(String senderId, String recipientId, Handler> handler); + Future verify(String senderId, String recipientId); - void getDiscoverVisibleUsers(String userId, JsonObject filter, final Handler> handler); + Future getDiscoverVisibleUsers(String userId, DiscoverVisibleFilterDTO filter); - void getDiscoverVisibleStructures(final Handler> handler); + Future getDiscoverVisibleStructures(); - void discoverVisibleAddCommuteUsers(UserInfos user, String recipientId, HttpServerRequest request, Handler> handler); + Future discoverVisibleAddCommuteUsers(UserInfos user, String recipientId, HttpServerRequest request); - void discoverVisibleRemoveCommuteUsers(String senderId, String recipientId, Handler> handler); + Future discoverVisibleRemoveCommuteUsers(String senderId, String recipientId); - void discoverVisibleGetGroups(String userId, Handler> handler); + Future discoverVisibleGetGroups(String userId); - void discoverVisibleGetUsersInGroup(String userId, String groupId, Handler> handler); + Future discoverVisibleGetUsersInGroup(String userId, String groupId); - void createDiscoverVisibleGroup(String userId, JsonObject body, Handler> handler); + Future createDiscoverVisibleGroup(String userId, DiscoverVisibleGroupBodyDTO body); - void updateDiscoverVisibleGroup(String userId, String groupId, JsonObject body, Handler> handler); + Future updateDiscoverVisibleGroup(String userId, String groupId, DiscoverVisibleGroupBodyDTO body); - void addDiscoverVisibleGroupUsers(UserInfos user, String groupId, JsonObject body, HttpServerRequest request, Handler> handler); + Future addDiscoverVisibleGroupUsers(UserInfos user, String groupId, DiscoverModifyGroupUsersDTO body, HttpServerRequest request); - void getDiscoverVisibleAcceptedProfile(UserInfos user, Handler> handler); + Future getDiscoverVisibleAcceptedProfile(UserInfos user); /** diff --git a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java index 59072e61f0..f702dc3112 100644 --- a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java +++ b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java @@ -30,6 +30,7 @@ import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import org.apache.commons.collections4.CollectionUtils; import org.entcore.common.conversation.LegacySearchVisibleRequest; import org.entcore.common.neo4j.Neo4j; import org.entcore.common.neo4j.StatementsBuilder; @@ -39,12 +40,18 @@ import org.entcore.common.user.UserUtils; import org.entcore.common.utils.StringUtils; import org.entcore.common.validation.StringValidation; +import org.entcore.communication.dto.rest.DiscoverVisibleGroupBodyDTO; +import org.entcore.communication.dto.rest.DiscoverModifyGroupUsersDTO; +import org.entcore.communication.dto.rest.DiscoverVisibleFilterDTO; +import org.entcore.communication.dto.rest.SearchVisibleRequestDTO; import org.entcore.communication.services.CommunicationService; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import static fr.wseduc.webutils.Utils.eitherToFuture; +import static fr.wseduc.webutils.Utils.isNotEmpty; import static io.vertx.core.json.JsonObject.mapFrom; import static org.entcore.common.neo4j.Neo4jResult.*; import static org.entcore.common.share.ShareService.EXPECTED_IDS_USERS_GROUPS; @@ -378,8 +385,21 @@ public void communiqueWith(String groupId, Handler> h } @Override - public void addLinkBetweenRelativeAndStudent(String groupId, Direction direction, - Handler> handler) { + public Future communiqueWith(String groupId) { + Promise promise = Promise.promise(); + communiqueWith(groupId, r -> { + if (r.isLeft()) { + promise.fail(r.left().getValue()); + } else { + promise.complete(r.right().getValue()); + } + }); + return promise.future(); + } + + @Override + public Future addLinkBetweenRelativeAndStudent(String groupId, Direction direction) { + Promise promise = Promise.promise(); String createRelationship; switch (direction) { case INCOMING: @@ -397,12 +417,19 @@ public void addLinkBetweenRelativeAndStudent(String groupId, Direction direction "CREATE UNIQUE " + createRelationship + "RETURN COUNT(*) as number "; JsonObject params = new JsonObject().put("groupId", groupId).put("direction", direction.name()); - neo4j.execute(query, params, validUniqueResultHandler(handler)); + neo4j.execute(query, params, validUniqueResultHandler( res -> { + if(res.isLeft()) { + promise.fail(res.left().getValue()); + return; + } + promise.complete(res.right().getValue().getInteger("number")); + })); + return promise.future(); } @Override - public void removeLinkBetweenRelativeAndStudent(String groupId, Direction direction, - Handler> handler) { + public Future removeLinkBetweenRelativeAndStudent(String groupId, Direction direction) { + Promise promise = Promise.promise(); String relationship; String set; switch (direction) { @@ -429,7 +456,14 @@ public void removeLinkBetweenRelativeAndStudent(String groupId, Direction direct "DELETE r " + "RETURN COUNT(*) as number "; JsonObject params = new JsonObject().put("groupId", groupId); - neo4j.execute(query, params, validUniqueResultHandler(handler)); + neo4j.execute(query, params, validUniqueResultHandler(res -> { + if(res.isLeft()) { + promise.fail(res.left().getValue()); + return; + } + promise.complete(res.right().getValue().getInteger("number")); + })); + return promise.future(); } private List getStatementsForDefaultRules(JsonArray structureIds, JsonObject defaultRules) { @@ -504,6 +538,47 @@ public void initDefaultRules(JsonArray structureIds, JsonObject defaultRules, initDefaultRules(structureIds, defaultRules, null, true, handler); } + @Override + public Future initDefaultRules(JsonArray structureIds, JsonObject defaultRules) { + return eitherToFuture(h -> initDefaultRules(structureIds, defaultRules, h)); + } + + @Override + public Future initDefaultRules(JsonArray structureIds, JsonObject defaultRules, + Integer transactionId, Boolean commit) { + return eitherToFuture(h -> initDefaultRules(structureIds, defaultRules, transactionId, commit, h)); + } + + @Override + public Future applyDefaultRules(JsonArray structureIds) { + return eitherToFuture(h -> applyDefaultRules(structureIds, h)); + } + + @Override + public Future applyDefaultRules(JsonArray structureIds, Integer transactionId, Boolean commit) { + return eitherToFuture(h -> applyDefaultRules(structureIds, transactionId, commit, h)); + } + + @Override + public Future applyRules(String groupId) { + return eitherToFuture(h -> applyRules(groupId, h)); + } + + @Override + public Future addLink(String startGroupId, String endGroupId) { + return eitherToFuture(h -> addLink(startGroupId, endGroupId, h)); + } + + @Override + public Future addLinkWithUsers(String groupId, Direction direction) { + return eitherToFuture(h -> addLinkWithUsers(groupId, direction, h)); + } + + @Override + public Future removeLinkWithUsers(String groupId, Direction direction) { + return eitherToFuture(h -> removeLinkWithUsers(groupId, direction, h)); + } + private void getStatementsForDefaultRules(JsonArray structureIds, String attr, JsonObject defaultRules, final StatementsBuilder existingGroups, final StatementsBuilder newGroups) { final String[] a = attr.split("\\-"); @@ -721,7 +796,7 @@ public void applyRules(String groupId, Handler> handl } @Override - public void removeRules(String structureId, Handler> handler) { + public Future removeRules(String structureId) { String query; JsonObject params = new JsonObject(); if (structureId != null && !structureId.trim().isEmpty()) { @@ -736,7 +811,7 @@ public void removeRules(String structureId, Handler> "OPTIONAL MATCH ()-[r1:COMMUNIQUE_DIRECT]->() " + "DELETE r, r1 "; } - neo4j.execute(query, params, validEmptyHandler(handler)); + return eitherToFuture(h -> neo4j.execute(query, params, validEmptyHandler(h))); } @Override @@ -864,6 +939,105 @@ public void visibleUsers( neo4j.execute(q, params, validResultHandler(handler)); } + @Override + public Future visibleUsers(UserInfos userInfos, SearchVisibleRequestDTO searchVisibleDto) { + Promise promise = Promise.promise(); + String preFilter = ""; + String match = ""; + String where = ""; + String nbUsers = ""; + String groupTypes = ""; + JsonObject params = new JsonObject(); + JsonArray expectedTypes = null; + + boolean matchAdded = false; + + if (!CollectionUtils.isEmpty(searchVisibleDto.getStructures())) { + + match = "MATCH "; + where = " WHERE "; + match += "(visibles)-[:IN*0..1]->()-[:DEPENDS*1..2]->(n) "; + where += "n.id IN {nIds} "; + + params.put("nIds", new JsonArray(searchVisibleDto.getStructures())); + matchAdded = true; + } else if (!CollectionUtils.isEmpty(searchVisibleDto.getClasses())) { + + match = "MATCH "; + where = " WHERE "; + match += "(visibles)-[:IN*0..1]->()-[:DEPENDS*1..2]->(n) "; + where += "n.id IN {nIds} "; + + params.put("nIds", new JsonArray(searchVisibleDto.getClasses())); + matchAdded = true; + } + if (!CollectionUtils.isEmpty(searchVisibleDto.getProfiles())) { + params.put("filters", new JsonArray(searchVisibleDto.getProfiles())); + if (!matchAdded) { + match = "MATCH (visibles)-[:IN*0..1]->(g)"; + where = " WHERE g.filter IN {filters} "; + } else { + match += " MATCH (visibles)-[:IN*0..1]->(g)"; + where += " AND g.filter IN {filters} "; + } + matchAdded = true; + } + if (!CollectionUtils.isEmpty(searchVisibleDto.getFunctions())) { + params.put("filters2", new JsonArray(searchVisibleDto.getFunctions())); + if (!matchAdded) { + match = "MATCH (visibles)-[:IN*0..1]->(g2)"; + where = " WHERE g2.filter IN {filters2} "; + } else { + match += " MATCH (visibles)-[:IN*0..1]->(g2)"; + where += " AND g2.filter IN {filters2} "; + } + matchAdded = true; + } + if (!CollectionUtils.isEmpty(searchVisibleDto.getPositions())) { + params.put("positionIds", new JsonArray(searchVisibleDto.getPositions())); + if (!matchAdded) { + where = " WHERE "; + } else { + where += " AND "; + } + where += " ANY(id IN positionIds WHERE id IN {positionIds}) "; + matchAdded = true; + } + if (isNotEmpty(searchVisibleDto.getSearch())) { + preFilter = "AND m.displayNameSearchField CONTAINS {search} "; + + String sanitizedSearch = StringValidation.sanitize(searchVisibleDto.getSearch()); + params.put("search", sanitizedSearch); + } + if (CollectionUtils.isEmpty(searchVisibleDto.getTypes())) { + if (CommunicationService.EXPECTED_TYPES.containsAll(searchVisibleDto.getTypes())) { + expectedTypes = new JsonArray(searchVisibleDto.getTypes()); + } + } + if (Boolean.TRUE.equals(searchVisibleDto.getNbUsersInGroups())) { + nbUsers = ", visibles.nbUsers as nbUsers"; + } + if (Boolean.TRUE.equals(searchVisibleDto.getGroupType())) { + groupTypes = ", labels(visibles) as groupType, visibles.filter as groupProfile"; + } + + final String customReturn = match + where + + "RETURN DISTINCT visibles.id as id, visibles.name as name, " + + "positionNames, positionIds, " + + "visibles.displayName as displayName, visibles.groupDisplayName as groupDisplayName, " + + "HEAD(visibles.profiles) as profile, subjects" + nbUsers + groupTypes; + + visibleUsers(userInfos.getUserId(), null, expectedTypes, searchVisibleDto.isItSelf(), searchVisibleDto.isMyGroup(), searchVisibleDto.isProfile(), + preFilter, customReturn, params, userInfos.getType(), false, visibles -> { + if (visibles.isRight()) { + promise.complete(visibles.right().getValue()); + } else { + promise.fail(visibles.left().getValue()); + } + }); + return promise.future(); + } + @Override public void usersCanSeeMe(String userId, Handler> handler) { @@ -935,6 +1109,35 @@ public void visibleManualGroups(String userId, String customReturn, JsonObject a neo4j.execute(query, params, validResultHandler(handler)); } + @Override + public Future visibleUsers(String userId, String structureId, JsonArray expectedTypes, boolean itSelf, + boolean myGroup, boolean profile, String preFilter, String customReturn, JsonObject additionalParams, + String userProfile, boolean reverseUnion) { + return eitherToFuture(h -> visibleUsers(userId, structureId, expectedTypes, itSelf, myGroup, profile, + preFilter, customReturn, additionalParams, userProfile, reverseUnion, h)); + } + + @Override + public Future usersCanSeeMe(String userId) { + return eitherToFuture(h -> usersCanSeeMe(userId, h)); + } + + @Override + public Future visibleProfilsGroups(String userId, String customReturn, JsonObject additionnalParams, + String preFilter) { + return eitherToFuture(h -> visibleProfilsGroups(userId, customReturn, additionnalParams, preFilter, h)); + } + + @Override + public Future visibleManualGroups(String userId, String customReturn, JsonObject additionnalParams) { + return eitherToFuture(h -> visibleManualGroups(userId, customReturn, additionnalParams, h)); + } + + @Override + public Future visibleUsersForShare(String userId, String search, JsonArray userIds) { + return eitherToFuture(h -> visibleUsersForShare(userId, search, userIds, h)); + } + private static String relationQuery = "OPTIONAL MATCH (sg:Structure)<-[:DEPENDS]-(g) " + "OPTIONAL MATCH (sc:Structure)<-[:BELONGS]-(c:Class)<-[:DEPENDS]-(g) " + "WITH COALESCE(sg, sc) as s, c, g " @@ -956,26 +1159,44 @@ public void visibleManualGroups(String userId, String customReturn, JsonObject a + " WHEN HAS(g.subType) THEN g.subType END as subType"; @Override - public void getOutgoingRelations(String id, Handler> results) { + public Future getOutgoingRelations(String id) { + Promise promise = Promise.promise(); String query = "MATCH (g:Group)<-[:COMMUNIQUE]-(ug: Group { id: {id} }) WHERE exists(g.id) " + relationQuery; JsonObject params = new JsonObject().put("id", id); - neo4j.execute(query, params, validResultHandler(results)); + neo4j.execute(query, params, validResultHandler( v -> { + if(v.isLeft()) { + promise.fail(v.left().getValue()); + return; + } + promise.complete(v.right().getValue()); + })); + + return promise.future(); } @Override - public void getIncomingRelations(String id, Handler> results) { + public Future getIncomingRelations(String id) { + Promise promise = Promise.promise(); String query = "MATCH (g:Group)-[:COMMUNIQUE]->(ug: Group { id: {id} }) WHERE exists(g.id) " + relationQuery; JsonObject params = new JsonObject().put("id", id); - neo4j.execute(query, params, validResultHandler(results)); + neo4j.execute(query, params, validResultHandler(v -> { + if(v.isLeft()) { + promise.fail(v.left().getValue()); + return; + } + promise.complete(v.right().getValue()); + })); + return promise.future(); } @Override - public void safelyRemoveLinkWithUsers(String groupId, Handler> handler) { + public Future safelyRemoveLinkWithUsers(String groupId) { + Promise promise = Promise.promise(); getRelationsOfGroup(groupId).whenComplete((result, err) -> { if (err != null) { - handler.handle(new Either.Left<>(err.getMessage())); + promise.fail(err.getMessage()); } else { int numberOfSendingGroups = result.getInteger("numberOfSendingGroups"); int numberOfReceivingGroups = result.getInteger("numberOfReceivingGroups"); @@ -983,14 +1204,15 @@ public void safelyRemoveLinkWithUsers(String groupId, Handler 0, numberOfReceivingGroups > 0); if (directionToRemove == null) { - handler.handle(new Either.Left<>(CommunicationService.IMPOSSIBLE_TO_CHANGE_DIRECTION)); + promise.fail(CommunicationService.IMPOSSIBLE_TO_CHANGE_DIRECTION); } else { Direction nextDirection = computeNextDirection(directionToRemove); removeLinkWithUsers(groupId, directionToRemove, - t -> handler.handle(new Either.Right<>(new JsonObject().put("users", nextDirection)))); + t -> promise.complete(new JsonObject().put("users", nextDirection))); } } }); + return promise.future(); } @Override @@ -1115,11 +1337,8 @@ protected Future computeWarningMessageForAddLinkCheck( } @Override - public void addLinkCheckOnly(String startGroupId, String endGroupId, UserInfos userInfos, Handler> handler) { - // 1. Get group info (Direction, Type and Subtype) - getGroupInfos(startGroupId, endGroupId) - // 2. Compute next directions - // 3. Check for impossible direction changes + public Future addLinkCheckOnly(String startGroupId, String endGroupId, UserInfos userInfos) { + return getGroupInfos(startGroupId, endGroupId) .compose(groupsInfos -> computeWarningMessageForAddLinkCheck( userInfos, groupsInfos[0], @@ -1127,9 +1346,7 @@ public void addLinkCheckOnly(String startGroupId, String endGroupId, UserInfos u groupsInfos[1], computeDirectionForAddLinkCheck(groupsInfos[1], false) )) - // 4. Compute warning message - .onSuccess(msg -> handler.handle(new Either.Right<>(new JsonObject().put("warning", msg)))) - .onFailure(err -> handler.handle(new Either.Left(err.getMessage()))); + .map(msg -> new JsonObject().put("warning", msg)); } private CompletableFuture getRelationsOfGroup(String groupId) { @@ -1151,10 +1368,11 @@ private CompletableFuture getRelationsOfGroup(String groupId) { } @Override - public void removeRelations(String sendingGroupId, String receivingGroupId, Handler> handler) { + public Future removeRelations(String sendingGroupId, String receivingGroupId) { + Promise promise = Promise.promise(); this.removeLink(sendingGroupId, receivingGroupId, r -> { if (r.isLeft()) { - handler.handle(r); + promise.fail(r.left().getValue()); } else { List> futures = new ArrayList<>(); futures.add(getRelationsOfGroup(sendingGroupId) @@ -1200,36 +1418,30 @@ public void removeRelations(String sendingGroupId, String receivingGroupId, Hand .thenApply(a -> futures.stream().map(CompletableFuture::join).collect(Collectors.toList())) .whenComplete((re, err) -> { if (err != null) { - handler.handle(new Either.Left<>(err.getMessage())); + promise.fail(err.getMessage()); } else { Direction senderDirection = re.get(0); Direction receiverDirection = re.get(1); - - handler.handle(new Either.Right<>(new JsonObject() + promise.complete(new JsonObject() .put("sender", senderDirection != null ? senderDirection.toString() : null) - .put("receiver", receiverDirection != null ? receiverDirection.toString() : null) - )); + .put("receiver", receiverDirection != null ? receiverDirection.toString() : null)); } }); } }); + return promise.future(); } @Override - public void processChangeDirectionAfterAddingLink(String startGroupId, String endGroupId, Handler> handler) { - // 1. Get group info (Direction, Type and Subtype) - getGroupInfos(startGroupId, endGroupId) - // 2. Compute next directions - // 3. Apply direction changes + public Future processChangeDirectionAfterAddingLink(String startGroupId, String endGroupId) { + return getGroupInfos(startGroupId, endGroupId) .compose(groupsInfos -> { final Direction fromStartDirection = Direction.fromString(groupsInfos[0].getString("internalCommunicationRule")); final Direction fromEndDirection = Direction.fromString(groupsInfos[1].getString("internalCommunicationRule")); final Direction toStartDirection = computeDirectionForAddLinkCheck(groupsInfos[0], true); final Direction toEndDirection = computeDirectionForAddLinkCheck(groupsInfos[1], false); - // Check if any rule was added to the start group Direction addedStartRule = Direction.fromBitmask(toStartDirection.bitmask & ~fromStartDirection.bitmask); - // Check if any rule was added to the end group Direction addedEndRule = Direction.fromBitmask(toEndDirection.bitmask & ~fromEndDirection.bitmask); if (addedStartRule.equals(Direction.NONE) && addedEndRule.equals(Direction.NONE)) { @@ -1255,10 +1467,7 @@ public void processChangeDirectionAfterAddingLink(String startGroupId, String en }); return promise.future(); } - }) - // 4. Compute warning message - .onSuccess(result -> handler.handle(new Either.Right(result))) - .onFailure(err -> handler.handle(new Either.Left(err.getMessage()))); + }); } public Direction computeDirectionToRemove(boolean hasIncomingRelationship, boolean hasOutgoingRelationship) { @@ -1309,7 +1518,11 @@ public boolean isImpossibleToChangeDirectionGroupForAddLink(String filter, Strin } @Override - public void verify(String senderId, String recipientId, Handler> handler) { + public Future verify(String senderId, String recipientId) { + return eitherToFuture(h -> verifyCallback(senderId, recipientId, h)); + } + + private void verifyCallback(String senderId, String recipientId, Handler> handler) { String query = "MATCH (s:User), (r:User) " + "where s.id = {senderId} and r.id = {recipientId} " @@ -1357,60 +1570,35 @@ public void verify(String senderId, String recipientId, Handler> handler) { - + public Future getDiscoverVisibleUsers(String userId, DiscoverVisibleFilterDTO filter) { JsonObject params = new JsonObject().put("userId", userId); StringBuilder query = new StringBuilder("MATCH (m:Visible) " + "WHERE (NOT(HAS(m.blocked)) OR m.blocked = false) AND m.id <> {userId} "); - if (filters != null && !filters.isEmpty()) { - for (String key : filters.fieldNames()) { - switch (key) { - case "structures": - JsonArray structures = filters.getJsonArray(key); - if (structures != null && !structures.isEmpty()) { - query.append("AND ANY(x IN m.structures WHERE x IN {structures}) "); - params.put("structures", structures); - } - break; - case "profiles": - JsonArray profile = filters.getJsonArray(key); - boolean allowProfileFilter = true; - - if(profile != null && !profile.isEmpty()) { - for (Object p : profile) { - if (!discoverVisibleExpectedProfile.contains(p)) { - allowProfileFilter = false; - break; - } - } - } else { - allowProfileFilter = false; - } + if (filter != null && filter.getStructures() != null && !filter.getStructures().isEmpty()) { + JsonArray structures = new JsonArray(filter.getStructures()); + query.append("AND ANY(x IN m.structures WHERE x IN {structures}) "); + params.put("structures", structures); + } - if (allowProfileFilter) { - query.append("AND HEAD(m.profiles) IN {profiles} "); - params.put("profiles", profile); - } else { - query.append("AND HEAD(m.profiles) IN {discoverVisibleExpectedProfile} "); - params.put("discoverVisibleExpectedProfile", discoverVisibleExpectedProfile); - } - break; - case "search": - final String search = filters.getString(key); - if (search != null && !search.trim().isEmpty()) { - query.append("AND m.displayNameSearchField CONTAINS {search} "); - String sanitizedSearch = StringValidation.sanitize(search); - params.put("search", sanitizedSearch); - } - break; - } + if (filter != null && filter.getProfiles() != null && !filter.getProfiles().isEmpty()) { + JsonArray profiles = new JsonArray(filter.getProfiles()); + boolean allowProfileFilter = profiles.stream().allMatch(discoverVisibleExpectedProfile::contains); + if (allowProfileFilter) { + query.append("AND HEAD(m.profiles) IN {profiles} "); + params.put("profiles", profiles); + } else { + query.append("AND HEAD(m.profiles) IN {discoverVisibleExpectedProfile} "); + params.put("discoverVisibleExpectedProfile", discoverVisibleExpectedProfile); } + } else { + query.append("AND HEAD(m.profiles) IN {discoverVisibleExpectedProfile} "); + params.put("discoverVisibleExpectedProfile", discoverVisibleExpectedProfile); } - if(filters == null || filters.isEmpty() || !filters.containsKey("profiles")) { - query.append("AND HEAD(m.profiles) in {discoverVisibleExpectedProfile} "); - params.put("discoverVisibleExpectedProfile", discoverVisibleExpectedProfile); + if (filter != null && filter.getSearch() != null && !filter.getSearch().trim().isEmpty()) { + query.append("AND m.displayNameSearchField CONTAINS {search} "); + params.put("search", StringValidation.sanitize(filter.getSearch())); } query.append("WITH DISTINCT m as visibles " @@ -1418,18 +1606,18 @@ public void getDiscoverVisibleUsers(String userId, JsonObject filters, final Han + "return DISTINCT visibles.id as id, visibles.name as name, visibles.displayName as displayName, " + "visibles.groupDisplayName as groupDisplayName, HEAD(visibles.profiles) as profile, visibles.structures as structures, u IS NOT NULL as hasCommunication"); - neo4j.execute(query.toString(), params, validResultHandler(handler)); + return eitherToFuture(h -> neo4j.execute(query.toString(), params, validResultHandler(h))); } /** * Return the list of structures * */ @Override - public void getDiscoverVisibleStructures(final Handler> handler) { + public Future getDiscoverVisibleStructures() { String query = "MATCH (s:Structure) " + "RETURN s.id as id, s.externalId as type, s.name as label, 'false' as checked"; - neo4j.execute(query, new JsonObject(), validResultHandler(handler)); + return eitherToFuture(h -> neo4j.execute(query, new JsonObject(), validResultHandler(h))); } @@ -1437,77 +1625,70 @@ public void getDiscoverVisibleStructures(final Handler * Add communication between two users, using the COMMUNIQUE_DIRECT relation and source 'MANUAL' * */ @Override - public void discoverVisibleAddCommuteUsers(UserInfos user, String recipientId, HttpServerRequest request, Handler> handler){ - + public Future discoverVisibleAddCommuteUsers(UserInfos user, String recipientId, HttpServerRequest request) { String query = "MATCH (s:User {id : {senderId}}), (r:User {id : {recipientId}}) WHERE HEAD(s.profiles) IN {discoverVisibleExpectedProfile} AND HEAD(r.profiles) IN {discoverVisibleExpectedProfile} " + "OPTIONAL MATCH (s)-[rel:COMMUNIQUE_DIRECT]->(r) WITH s, r, COUNT(rel) AS relCount " + "WHERE relCount = 0 CREATE (s)-[:COMMUNIQUE_DIRECT {source: 'MANUAL'}]->(r) RETURN COUNT(*) AS number "; JsonObject params = new JsonObject().put("senderId", user.getUserId()).put("recipientId", recipientId).put("discoverVisibleExpectedProfile", discoverVisibleExpectedProfile); - neo4j.execute(query, params, validUniqueResultHandler(result -> { + return eitherToFuture(h -> neo4j.execute(query, params, validUniqueResultHandler(result -> { if (result.isRight() && !result.right().getValue().isEmpty()) { - if(result.right().getValue().getInteger("number") > 0) { + if (result.right().getValue().getInteger("number") > 0) { sendNotificationTimeline(request, user, new JsonArray().add(recipientId), ""); } - handler.handle(new Either.Right<>(result.right().getValue())); - } else { - if (result.isLeft()) { - handler.handle(new Either.Left<>(result.left().getValue())); - - } + h.handle(new Either.Right<>(result.right().getValue())); + } else if (result.isLeft()) { + h.handle(new Either.Left<>(result.left().getValue())); } - })); + }))); } /** * Remove communication between two users, using the COMMUNIQUE_DIRECT relation and source 'MANUAL' * */ @Override - public void discoverVisibleRemoveCommuteUsers(String senderId, String recipientId, Handler> handler){ - + public Future discoverVisibleRemoveCommuteUsers(String senderId, String recipientId) { String query = "MATCH (s:User {id : {senderId}})-[r:COMMUNIQUE_DIRECT { source: 'MANUAL'}]-(u:User {id : {recipientId}}) " + "DELETE r " + "RETURN COUNT(*) as number"; JsonObject params = new JsonObject().put("senderId", senderId).put("recipientId", recipientId); - neo4j.execute(query, params, validUniqueResultHandler(handler)); + return eitherToFuture(h -> neo4j.execute(query, params, validUniqueResultHandler(h))); } /** * Return the list of groups, that the user is IN or has COMMUNIQUE rights, with group type 'manager' * */ @Override - public void discoverVisibleGetGroups(String userId, Handler> handler) { + public Future discoverVisibleGetGroups(String userId) { String query = "MATCH (g:CommunityGroup:Group:Visible {type: 'manager'})<-[r:IN|COMMUNIQUE]-(u:User {id: {userId}}) " + "RETURN DISTINCT g.id as id, g.name as name, g.displayNameSearchField as displayName, g.nbUsers as nbUsers ORDER BY name"; JsonObject params = new JsonObject().put("userId", userId); - neo4j.execute(query, params, validResultHandler(handler)); + return eitherToFuture(h -> neo4j.execute(query, params, validResultHandler(h))); } /** * Return the list of users, that are IN the group, with group type 'manager' * */ @Override - public void discoverVisibleGetUsersInGroup(String userId, String groupId, Handler> handler) { + public Future discoverVisibleGetUsersInGroup(String userId, String groupId) { String query = "MATCH (g:CommunityGroup:Group:Visible {id: {groupId}, type: 'manager'})<-[r:IN|COMMUNIQUE]-(u:User) "+ "OPTIONAL MATCH (m:User {id: {userId}})-[:COMMUNIQUE_DIRECT {source:'MANUAL'}]-(u) " + "RETURN DISTINCT HEAD(u.profiles) as type, u.id as id, u.displayName as displayName, m IS NOT NULL as hasCommunication, u.login as login ORDER BY type DESC, displayName"; JsonObject param = new JsonObject().put("userId", userId).put("groupId", groupId); - neo4j.execute(query, param, validResultHandler(handler)); + return eitherToFuture(h -> neo4j.execute(query, param, validResultHandler(h))); } /** * Create a new group, with group type 'manager' * */ @Override - public void createDiscoverVisibleGroup(String userId, JsonObject body, Handler> handler){ - - + public Future createDiscoverVisibleGroup(String userId, DiscoverVisibleGroupBodyDTO body) { String query = "CREATE (g:CommunityGroup:Group:Visible {name : {name}, type : 'manager', users : 'BOTH', displayNameSearchField: {name}, filter : 'CommunityManager'}) " + "SET g.id = id(g) +'-'+timestamp() " + "WITH g " + @@ -1515,39 +1696,23 @@ public void createDiscoverVisibleGroup(String userId, JsonObject body, Handlerg, u-[:COMMUNIQUE]->g " + "RETURN g.id as id"; - final String name = body.getString("name"); - - if (name == null || name.trim().isEmpty()) { - handler.handle(new Either.Left<>("Invalid name")); - return; - } - - JsonObject params = new JsonObject().put("userId", userId).put("name", name); - - neo4j.execute(query, params, validUniqueResultHandler(handler)); + JsonObject params = new JsonObject().put("userId", userId).put("name", body.getName()); + return eitherToFuture(h -> neo4j.execute(query, params, validUniqueResultHandler(h))); } /** * Update the name of a group, with group type 'manager' and groupId * */ @Override - public void updateDiscoverVisibleGroup(String userId, String groupId, JsonObject body, Handler> handler){ - - final String name = body.getString("name"); - - if (name != null && !name.trim().isEmpty()) { - String query = "MATCH (g:CommunityGroup:Group:Visible {id : {groupId}}) " + - "SET g.name = {name}, g.displayNameSearchField = {name} " + - "RETURN g.id as id"; + public Future updateDiscoverVisibleGroup(String userId, String groupId, DiscoverVisibleGroupBodyDTO body) { + String query = "MATCH (g:CommunityGroup:Group:Visible {id : {groupId}}) " + + "SET g.name = {name}, g.displayNameSearchField = {name} " + + "RETURN g.id as id"; - JsonObject params = new JsonObject().put("groupId", groupId).put("name", name); + JsonObject params = new JsonObject().put("groupId", groupId).put("name", body.getName()); - neo4j.execute(query, params, validUniqueResultHandler(handler)); - - } else { - handler.handle(new Either.Left<>("Invalid name")); - } + return eitherToFuture(h -> neo4j.execute(query, params, validUniqueResultHandler(h))); } /** @@ -1556,34 +1721,22 @@ public void updateDiscoverVisibleGroup(String userId, String groupId, JsonObject * Update the number of users in the group * */ @Override - public void addDiscoverVisibleGroupUsers(UserInfos user, String groupId, JsonObject body, HttpServerRequest request, Handler> handler) { + public Future addDiscoverVisibleGroupUsers(UserInfos user, String groupId, DiscoverModifyGroupUsersDTO body, HttpServerRequest request) { StatementsBuilder statementsBuilder = new StatementsBuilder(); - JsonArray oldUsers = body.getJsonArray("oldUsers"); - JsonArray newUsers = body.getJsonArray("newUsers"); + JsonArray oldUsers = new JsonArray(body.getOldUsers()); + JsonArray newUsers = new JsonArray(body.getNewUsers()); JsonObject params = new JsonObject().put("groupId", groupId); - if (newUsers == null || newUsers.isEmpty()) { - handler.handle(new Either.Left<>("Invalid users")); - return; - } - - if (newUsers.size() > 100) { - handler.handle(new Either.Left<>("Too many users")); - return; - } - - if(!newUsers.contains(user.getUserId()) && oldUsers.isEmpty()){ + if (!newUsers.contains(user.getUserId()) && oldUsers.isEmpty()) { newUsers.add(user.getUserId()); } JsonArray usersToRemove = new JsonArray(); JsonArray usersToAdd = new JsonArray(); - if (!oldUsers.isEmpty()) { - for (int i = 0; i < oldUsers.size(); i++) { - if (!newUsers.contains(oldUsers.getString(i))) { - usersToRemove.add(oldUsers.getString(i)); - } + for (int i = 0; i < oldUsers.size(); i++) { + if (!newUsers.contains(oldUsers.getString(i))) { + usersToRemove.add(oldUsers.getString(i)); } } @@ -1602,29 +1755,28 @@ public void addDiscoverVisibleGroupUsers(UserInfos user, String groupId, JsonObj "MATCH (u:User) " + "WHERE u.id IN {users} AND HEAD(u.profiles) IN {discoverVisibleExpectedProfile} " + "CREATE UNIQUE u-[:IN]->g, u-[:COMMUNIQUE]->g;"; - statementsBuilder.add(addQuery, params.copy().put("users", usersToAdd).put("discoverVisibleExpectedProfile", discoverVisibleExpectedProfile)); - final String queryNb = - "MATCH (g:CommunityGroup:Group:Visible {id : {groupId}})<-[:IN]-(u:User) " + + final String queryNb = "MATCH (g:CommunityGroup:Group:Visible {id : {groupId}})<-[:IN]-(u:User) " + "WITH g, count(distinct u) as cu " + "SET g.nbUsers = cu;"; - statementsBuilder.add(queryNb, params); + Promise promise = Promise.promise(); neo4j.executeTransaction(statementsBuilder.build(), null, true, event -> { if ("ok".equals(event.body().getString("status"))) { - if(!usersToAdd.isEmpty()){ - if(usersToAdd.contains(user.getUserId())) { + if (!usersToAdd.isEmpty()) { + if (usersToAdd.contains(user.getUserId())) { usersToAdd.remove(user.getUserId()); } sendNotificationTimeline(request, user, usersToAdd, groupId); } - handler.handle(new Either.Right<>(event.body())); + promise.complete(); } else { - handler.handle(new Either.Left<>(event.body().getString("message"))); + promise.fail(event.body().getString("message")); } }); + return promise.future(); } /** @@ -1676,12 +1828,11 @@ private void sendNotificationTimeline(HttpServerRequest request, UserInfos user, } @Override - public void getDiscoverVisibleAcceptedProfile(UserInfos user, Handler> handler) { - if(discoverVisibleExpectedProfile.isEmpty() || !discoverVisibleExpectedProfile.contains(user.getType())) { - handler.handle(new Either.Right<>(new JsonArray())); - return; + public Future getDiscoverVisibleAcceptedProfile(UserInfos user) { + if (discoverVisibleExpectedProfile.isEmpty() || !discoverVisibleExpectedProfile.contains(user.getType())) { + return Future.succeededFuture(new JsonArray()); } - handler.handle(new Either.Right<>(discoverVisibleExpectedProfile)); + return Future.succeededFuture(discoverVisibleExpectedProfile); } @Override diff --git a/conversation/frontend/package.json.template b/conversation/frontend/package.json.template index 8902b20cca..6381baadaf 100644 --- a/conversation/frontend/package.json.template +++ b/conversation/frontend/package.json.template @@ -32,11 +32,11 @@ "typecheck": "tsc -b --noEmit --pretty" }, "dependencies": { - "@edifice.io/react": "2.5.23", - "@edifice.io/bootstrap": "2.5.23", - "@edifice.io/client": "2.5.23", - "@edifice.io/utilities": "2.5.23", - "@edifice.io/tiptap-extensions": "2.5.23", + "@edifice.io/react": "%packageVersion%", + "@edifice.io/bootstrap": "%packageVersion%", + "@edifice.io/client": "%packageVersion%", + "@edifice.io/utilities": "%packageVersion%", + "@edifice.io/tiptap-extensions": "%packageVersion%", "@react-spring/web": "9.7.5", "@screeb/sdk-react": "0.6.0", "@tanstack/react-query": "5.90.19", diff --git a/directory/src/main/java/org/entcore/directory/listeners/DirectoryBrokerListenerImpl.java b/directory/src/main/java/org/entcore/directory/listeners/DirectoryBrokerListenerImpl.java index 09e94aa9f4..aeb775b674 100644 --- a/directory/src/main/java/org/entcore/directory/listeners/DirectoryBrokerListenerImpl.java +++ b/directory/src/main/java/org/entcore/directory/listeners/DirectoryBrokerListenerImpl.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; - import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; @@ -543,7 +542,7 @@ public Future getStructureUsers(GetStructureUsersR } // The boolean params might be used to filter deleted users - // TODO: update the method to find if the user is an admc or not (third params) + // TODO: update the method to find if the user is an admc or not (third params) // As we do not have acces to the user informations in the broker // we default to false for now // In directory > src > ... > controllers > StructureController.java: @@ -576,6 +575,7 @@ public Future getStructureUsers(GetStructureUsersR } + @Override public Future getUsersFromGroups(GetUsersFromGroupsRequestDTO request) { final Promise promise = Promise.promise(); diff --git a/package.json b/package.json index b9f5f12527..35251b604b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ent-core", - "version": "6.15.6", + "version": "6.15.6-dev.0", "description": "", "main": "gulpfile.js", "directories": { @@ -28,7 +28,7 @@ "angular-sanitize": "1.8.3", "axios": "0.15.3", "core-js": "^2.4.1", - "entcore": "4.8.17", + "entcore": "develop-enabling", "entcore-generic-icons": "https://github.com/edificeio/generic-icons.git", "entcore-toolkit": "^1.0.1", "humane-js": "^3.2.2", @@ -77,8 +77,8 @@ "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "merge2": "^1.0.3", - "ode-ngjs-front": "1.4.29", - "ode-ts-client": "1.2.7", + "ode-ngjs-front": "develop-enabling", + "ode-ts-client": "develop-enabling", "rxjs": "5.4.2", "sass-loader": "^13.0.2", "source-map-loader": "^0.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e44386d58f..22f4cc438c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^2.4.1 version: 2.6.12 entcore: - specifier: dev - version: 4.8.14-dev.202602171056(lodash@4.18.1) + specifier: develop-enabling + version: 4.8.16-develop-enabling.202602271110(lodash@4.18.1) entcore-generic-icons: specifier: https://github.com/edificeio/generic-icons.git version: https://codeload.github.com/edificeio/generic-icons/tar.gz/960f4af943706812f9ebae81a39b7f521bb2fc3b @@ -169,11 +169,11 @@ importers: specifier: ^1.0.3 version: 1.4.1 ode-ngjs-front: - specifier: dev - version: 1.4.23-dev.202602171058 + specifier: develop-enabling + version: 1.4.20-develop-enabling.202602061135 ode-ts-client: - specifier: dev - version: 1.2.5-dev.202603091504 + specifier: develop-enabling + version: 1.2.4-develop-enabling.202602061134 rxjs: specifier: 5.4.2 version: 5.4.2 @@ -234,8 +234,8 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} engines: {node: '>=6.9.0'} '@babel/core@7.29.0': @@ -272,10 +272,6 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -284,8 +280,8 @@ packages: resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true @@ -307,9 +303,6 @@ packages: '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -326,9 +319,6 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} @@ -516,11 +506,9 @@ packages: acorn-dynamic-import@2.0.2: resolution: {integrity: sha512-GKp5tQ8h0KMPWIYGRHHXI1s5tUpZixZ3IHF2jAu42wSCf6In/G873s6/y4DdKdhWvzhu1T6mE1JgvnhAKqyYYQ==} - deprecated: This is probably built in to whatever tool you're using. If you still need it... idk acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} - deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 @@ -544,11 +532,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - add-stream@1.0.0: resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} @@ -584,6 +567,9 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -600,21 +586,18 @@ packages: angular-route@1.8.3: resolution: {integrity: sha512-kpIcRmDR2+o1FxDVVYy8Rvfab86/7LDbOgTRb9T+X9ewPQiBRuDEnZtM3oJYBiQLvAXDYTJXHV48n/bGE9Mv2g==} - deprecated: For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward. angular-router-loader@0.5.0: resolution: {integrity: sha512-G3mcWH2x7rpD7kMuxrKtgBwW23TfBuJKt2OME7ghOmLy2Y4IgjUuP9GsHpNl6LmyY6U0URY0MSu3o1v+bGp5xg==} angular-sanitize@1.8.3: resolution: {integrity: sha512-2rxdqzlUVafUeWOwvY/FtyWk1pFTyCtzreeiTytG9m4smpuAEKaIJAjYeVwWsoV+nlTOcgpwV4W1OCmR+BQbUg==} - deprecated: For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward. angular2-template-loader@0.6.2: resolution: {integrity: sha512-jBSrm2yDsTA48GG0H57upn8rmTfJS3/R7EhUeAL/3ryWS8deT9You8UQKWpW4eVSEY7uMJ52iFwpOYXc8QEtGg==} angular@1.8.3: resolution: {integrity: sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==} - deprecated: For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward. ansi-colors@1.1.0: resolution: {integrity: sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==} @@ -699,7 +682,6 @@ packages: argv@0.0.2: resolution: {integrity: sha512-dEamhpPEwRUBpLNHeuCm/v+g0anFByHahxodVO/BbAarHVBBg2MccCwf9K+o1Pof+2btdnkJelYVUWjW/VrATw==} engines: {node: '>=0.6.10'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. arr-diff@1.1.0: resolution: {integrity: sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==} @@ -876,7 +858,6 @@ packages: axios@0.15.3: resolution: {integrity: sha512-w3/VNaraEcDri16lbemQWQGKfaFk9O0IZkzKlLeF5r6WWDv9TkcXkP+MWkRK8FbxwfozY/liI+qtvhV295t3HQ==} - deprecated: Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410 axios@0.21.1: resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==} @@ -992,10 +973,6 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - bonjour-service@1.3.0: resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} @@ -1005,7 +982,6 @@ packages: boom@2.10.1: resolution: {integrity: sha512-KbiZEa9/vofNcVJXGwdWWn25reQ3V3dHBWbS07FTF3/TOehLnm9GEhJV4T6ZvGPkShRpmUqYwnaCrkj0mRnP6Q==} engines: {node: '>=0.10.40'} - deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} @@ -1057,12 +1033,6 @@ packages: browserslist@1.7.7: resolution: {integrity: sha512-qHJblDE2bXVRYzuDetv/wAeHOJyO97+9wxC1cdCtyzgNuSozOyRCiiLaCR1f71AN66lQdVVBipWm63V+a7bPOw==} - deprecated: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools. - hasBin: true - - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true browserslist@4.28.2: @@ -1168,9 +1138,6 @@ packages: caniuse-lite@1.0.30001787: resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} - caniuse-lite@1.0.30001784: - resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} - caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -1413,7 +1380,6 @@ packages: conventional-changelog-cli@2.0.12: resolution: {integrity: sha512-6wh9W5Gpr9DM40E8cFi0qa6JotVm4Jq+suksuqgKnm544H8ZXsRhgGNXShDASOteY9brv9fX8/+fE/QL1wHqbA==} engines: {node: '>=6.9.0'} - deprecated: This package is no longer maintained. Please use the conventional-changelog package instead. hasBin: true conventional-changelog-codemirror@2.0.8: @@ -1485,7 +1451,6 @@ packages: copy-concurrently@1.0.5: resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==} - deprecated: This package is no longer supported. copy-descriptor@0.1.1: resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} @@ -1496,15 +1461,12 @@ packages: core-js@2.6.12: resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. core-js@3.8.3: resolution: {integrity: sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. core-js@3.9.1: resolution: {integrity: sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -1531,7 +1493,6 @@ packages: cryptiles@2.0.5: resolution: {integrity: sha512-FFN5KwpvvQTTS5hWPxrU8/QE4kQUc6uwZcrnlMBN82t1MgAtq8mnoDwINBly9Tdr02seeIIhtdF+UH1feBYGog==} engines: {node: '>=0.10.40'} - deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). crypto-browserify@3.12.1: resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} @@ -1807,9 +1768,6 @@ packages: electron-to-chromium@1.5.334: resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} - electron-to-chromium@1.5.330: - resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==} - elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -1876,8 +1834,8 @@ packages: lodash: ^4.17.1 typescript: 2.1.1 - entcore@4.8.14-dev.202602171056: - resolution: {integrity: sha512-pfDfTZ01kqGA+pnrbkRtTwyiy26Asu1kXzLoMewyuLsx7SFj1ul7eoYZ89xTD9zVSHRW7GrrIK05wiuTo4IUqA==} + entcore@4.8.16-develop-enabling.202602271110: + resolution: {integrity: sha512-Wg/v+Q/D1I5eRgJHj+RGBc/C2jvYmiiLgG0qr1KKUNEwBU4iYFmOPpVaiSD+v3AKJD5XtgXID41ryRxUvkCRJA==} entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} @@ -1889,9 +1847,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2104,7 +2059,6 @@ packages: figgy-pudding@3.5.2: resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} - deprecated: This module is no longer supported. file-loader@0.10.1: resolution: {integrity: sha512-MDhQNyTgdJpBnBveHgNgDwROzt+1YajNh2RL3fwhicFDPRReTcxsNnaHVUj1wVKU61VaqouQNsq2Ssiqxw4V+g==} @@ -2181,7 +2135,6 @@ packages: flatten@1.0.3: resolution: {integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==} - deprecated: flatten is deprecated in favor of utility frameworks such as lodash. flush-write-stream@1.1.1: resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==} @@ -2189,8 +2142,8 @@ packages: follow-redirects@1.0.0: resolution: {integrity: sha512-7s+wBk4z5xTwVJuozRBAyRofWKjD3uG2CUjZfZTrw9f+f+z8ZSxOjAqfIDLtc0Hnz+wGK2Y8qd93nGGjXBYKsQ==} - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -2253,7 +2206,6 @@ packages: fs-write-stream-atomic@1.0.10: resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==} - deprecated: This package is no longer supported. fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2326,7 +2278,6 @@ packages: git-semver-tags@2.0.3: resolution: {integrity: sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA==} engines: {node: '>=6.9.0'} - deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. hasBin: true gitconfiglocal@1.0.0: @@ -2359,7 +2310,6 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-modules@1.0.0: resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} @@ -2441,7 +2391,6 @@ packages: gulp-util@3.0.8: resolution: {integrity: sha512-q5oWPc12lwSFS9h/4VIjG+1NuNDlJ48ywV2JKItY4Ycc/n1fXJeYPVQsfu5ZrhQi7FGSDBalwUCLar/GyHXKGw==} engines: {node: '>=0.10'} - deprecated: gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5 gulp-watch@5.0.1: resolution: {integrity: sha512-HnTSBdzAOFIT4wmXYPDUn783TaYAq9bpaN05vuZNP5eni3z3aRx0NAKbjhhMYtcq76x4R1wf4oORDGdlrEjuog==} @@ -2474,12 +2423,10 @@ packages: har-validator@4.2.1: resolution: {integrity: sha512-5Gbp6RAftMYYV3UEI4c4Vv3+a4dQ7taVyvHt+/L6kRt+f4HX1GweAk5UDWN0SvdVnRBzGQ6OG89pGaD9uSFnVw==} engines: {node: '>=4'} - deprecated: this library is no longer supported har-validator@5.1.5: resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} engines: {node: '>=6'} - deprecated: this library is no longer supported hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} @@ -2561,10 +2508,13 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + hawk@3.1.3: resolution: {integrity: sha512-X8xbmTc1cbPXcQV4WkLcRMALuyoxhfpFATmyuCxJPOAvrDS4DNnsTAOmKUxMTOWU6TzrTOkxPKwIx5ZOpJVSrg==} engines: {node: '>=0.10.32'} - deprecated: This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} @@ -2576,7 +2526,6 @@ packages: hoek@2.16.3: resolution: {integrity: sha512-V6Yw1rIcYV/4JsnggjBU0l4Kr+EXhpwqXRusENU1Xx6ro00IHPHYNynCuBTOZAPlr3AAmLvchH9I7N/VUdvOwQ==} engines: {node: '>=0.10.40'} - deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). homedir-polyfill@1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} @@ -2637,10 +2586,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - http-parser-js@0.5.10: resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} @@ -2731,7 +2676,6 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.3: resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} @@ -2798,6 +2742,10 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + is-data-descriptor@1.0.1: resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} engines: {node: '>= 0.4'} @@ -3115,7 +3063,6 @@ packages: json3@3.3.2: resolution: {integrity: sha512-I5YLeauH3rIaE99EE++UeH2M2gSYo8/2TqDac7oZEH6D/DSQ4Woa628Qrfj1X9/OY5Mk5VvIDQaKCDchXaKrmA==} - deprecated: Please use the native JSON object instead of JSON 3 json5@0.5.1: resolution: {integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==} @@ -3295,7 +3242,6 @@ packages: lodash.clone@4.5.0: resolution: {integrity: sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==} - deprecated: This package is deprecated. Use structuredClone instead. lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} @@ -3332,7 +3278,6 @@ packages: lodash.pick@4.4.0: resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} - deprecated: This package is deprecated. Use destructuring assignment syntax instead. lodash.restparam@3.6.1: resolution: {integrity: sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==} @@ -3342,11 +3287,9 @@ packages: lodash.template@3.6.2: resolution: {integrity: sha512-0B4Y53I0OgHUJkt+7RmlDFWKjVAI/YUpWNiL9GQz5ORDr4ttgfQGo+phBWKFLJbBdtOwgMuUkdOHOnPg45jKmQ==} - deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. lodash.template@4.18.1: resolution: {integrity: sha512-5urZrLnV/VD6zHK5KsVtZgt7H19v51mIzoS0aBNH8yp3I8tbswrEjOABOPY8m8uB7NuibubLrMX+Y0PXsU9X+w==} - deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. lodash.templatesettings@3.1.1: resolution: {integrity: sha512-TcrlEr31tDYnWkHFWDCV3dHYroKEXpJZ2YJYvJdhN+y4AkWMDZ5I4I8XDtUKqSAyG81N7w+I1mFEJtcED+tGqQ==} @@ -3366,7 +3309,6 @@ packages: log4js@0.6.38: resolution: {integrity: sha512-Cd+klbx7lkiaamEId9/0odHxv/PFHDz2E12kEfd6/CzIOZD084DzysASR/Dot4i1dYPBQKC3r2XIER+dfbLOmw==} engines: {node: '>=0.8'} - deprecated: 0.x is no longer supported. Please upgrade to 6.x or higher. longest@1.0.1: resolution: {integrity: sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==} @@ -3604,7 +3546,6 @@ packages: move-concurrently@1.0.1: resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} - deprecated: This package is no longer supported. ms@0.7.1: resolution: {integrity: sha512-lRLiIR9fSNpnP6TC4v8+4OU7oStC01esuNowdQ34L+Gk8e5Puoc88IqJ+XAY/B3Mn2ZKis8l8HX90oU8ivzUHg==} @@ -3673,9 +3614,6 @@ packages: node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} - normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -3792,12 +3730,12 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - ode-ngjs-front@1.4.23-dev.202602171058: - resolution: {integrity: sha512-hIBRX6k7abAEWPh18OE7UP+mudtMMjXZ2daBnTuvqIDB3+UEj7OLJEI/WFG8wp99/h5y/8DJMfpy3EO5bhqS+g==} + ode-ngjs-front@1.4.20-develop-enabling.202602061135: + resolution: {integrity: sha512-3PYHuNeEcNGnzt98y2fat76kND4jak3H8E6/vauzvUF+VZdp8RdI41nkBvj1kieE/Efcf0INWW93wm3JU2V5AA==} engines: {node: 16 || 18} - ode-ts-client@1.2.5-dev.202603091504: - resolution: {integrity: sha512-7kUh00Aj1gsi7/bCXHO/rQf/3hNI7Cq07egudbJVfy5oVaFEhMKvxBtHeS5g9SK/x00AsfM8UvzP4eAnP7QdoQ==} + ode-ts-client@1.2.4-develop-enabling.202602061134: + resolution: {integrity: sha512-gke+c3mayfg6r8bxa9mqsDOPEpY1A0CD0orlX74ybwPs49RPZBYZ2Ng010Eoy2Ii8K8yoHVZw/SwqXMimd43zA==} engines: {node: 16 || 18} on-finished@2.3.0: @@ -4265,10 +4203,6 @@ packages: q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} engines: {node: '>=0.6.0', teleport: '>=0.2.0'} - deprecated: |- - You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qjobs@1.2.0: resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==} @@ -4324,10 +4258,6 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} - read-pkg-up@1.0.1: resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==} engines: {node: '>=0.10.0'} @@ -4485,7 +4415,6 @@ packages: request@2.88.2: resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} engines: {node: '>= 6'} - deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -4511,12 +4440,6 @@ packages: resolve-url@0.2.1: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} - deprecated: https://github.com/lydell/resolve-url#deprecated - - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} @@ -4548,12 +4471,10 @@ packages: rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true ripemd160@0.2.0: @@ -4653,8 +4574,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} engines: {node: '>=10'} hasBin: true @@ -4753,7 +4674,6 @@ packages: sntp@1.0.9: resolution: {integrity: sha512-7bgVOAnPj3XjrKY577S+puCKGCRlUrcrEdsMeRXlg9Ghf5df/xNi6sONUa43WrHUd3TjJBF7O04jYoiY0FVa0A==} engines: {node: '>=0.8.0'} - deprecated: This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. socket.io-adapter@0.5.0: resolution: {integrity: sha512-zmYvlFJay9skt4yk1MffE9p93HKvQtyy0BLZ5dRM73bOXFJXNZWq8qZVdY456sLaxdK6fHGiZ7glxzqvzwGzkw==} @@ -4792,7 +4712,6 @@ packages: source-map-resolve@0.5.3: resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} - deprecated: See https://github.com/lydell/source-map-resolve#deprecated source-map-support@0.4.18: resolution: {integrity: sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==} @@ -4802,7 +4721,6 @@ packages: source-map-url@0.4.1: resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} - deprecated: See https://github.com/lydell/source-map-url#deprecated source-map@0.1.43: resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} @@ -4893,10 +4811,6 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - stream-browserify@2.0.2: resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} @@ -5023,7 +4937,6 @@ packages: svgo@0.7.2: resolution: {integrity: sha512-jT/g9FFMoe9lu2IT6HtAxTA7RR2XOrmcrmCtGnyB/+GQnV6ZjNn+KOHZbZ35yL81+1F/aB6OeEsJztzBQ2EEwA==} engines: {node: '>=0.10.0'} - deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. hasBin: true symbol-observable@1.2.0: @@ -5042,10 +4955,6 @@ packages: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} - engines: {node: '>=6'} - tempfile@1.1.1: resolution: {integrity: sha512-NjT12fW6pSEKz1eVcADgaKfeM+XZ4+zSaqVz46XH7+CiEwcelnwtGWRRjF1p+xyW2PVgKKKS2UUw1LzRelntxg==} engines: {node: '>=0.10.0'} @@ -5071,11 +4980,6 @@ packages: engines: {node: '>=10'} hasBin: true - terser@5.46.1: - resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} - engines: {node: '>=10'} - hasBin: true - text-extensions@1.9.0: resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} engines: {node: '>=0.10'} @@ -5256,7 +5160,6 @@ packages: ua-parser-js@0.7.28: resolution: {integrity: sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==} - deprecated: You are using an outdated version of ua-parser-js. Please update to ua-parser-js v0.7.33 / v1.0.33 / v2.0.0 (or later) to avoid ReDoS vulnerability [CVE-2022-25927](https://github.com/advisories/GHSA-fhg7-m89q-25r3) ua-parser-js@2.0.9: resolution: {integrity: sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==} @@ -5357,12 +5260,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - upper-case@1.1.3: resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} @@ -5371,7 +5268,6 @@ packages: urix@0.1.0: resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} - deprecated: Please see https://github.com/lydell/urix#deprecated url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} @@ -5402,11 +5298,9 @@ packages: uuid@2.0.3: resolution: {integrity: sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true uuid@8.3.2: @@ -5731,7 +5625,7 @@ snapshots: dependency-graph: 0.11.0 magic-string: 0.26.7 reflect-metadata: 0.1.10 - semver: 7.7.4 + semver: 7.8.0 sourcemap-codec: 1.4.8 tslib: 2.8.1 typescript: 4.9.5 @@ -5749,7 +5643,7 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.3': {} '@babel/core@7.29.0': dependencies: @@ -5758,7 +5652,7 @@ snapshots: '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.3 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 @@ -5773,7 +5667,7 @@ snapshots: '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.3 '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -5781,7 +5675,7 @@ snapshots: '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.29.0 + '@babel/compat-data': 7.29.3 '@babel/helper-validator-option': 7.27.1 browserslist: 4.28.2 lru-cache: 5.1.1 @@ -5809,8 +5703,6 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.29.2': @@ -5818,14 +5710,14 @@ snapshots: '@babel/template': 7.28.6 '@babel/types': 7.29.0 - '@babel/parser@7.29.2': + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.3 '@babel/types': 7.29.0 '@babel/traverse@7.29.0': @@ -5833,7 +5725,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.3 '@babel/template': 7.28.6 '@babel/types': 7.29.0 debug: 4.4.3 @@ -5855,11 +5747,6 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/source-map@0.3.11': @@ -5876,11 +5763,6 @@ snapshots: '@kurkle/color@0.3.4': {} - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@leichtgewicht/ip-codec@2.0.5': {} '@ngtools/webpack@14.1.3(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0)(typescript@4.9.5))(typescript@4.9.5)(webpack@5.54.0(uglify-js@2.8.25))': @@ -6146,8 +6028,6 @@ snapshots: acorn@8.16.0: {} - acorn@8.16.0: {} - add-stream@1.0.0: {} after@0.8.2: {} @@ -6164,6 +6044,10 @@ snapshots: dependencies: ajv: 6.14.0 + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: ajv: 8.18.0 @@ -6182,6 +6066,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -6457,7 +6348,7 @@ snapshots: axios@0.21.1: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 transitivePeerDependencies: - debug @@ -6593,23 +6484,6 @@ snapshots: bn.js@5.2.3: {} - body-parser@1.20.4: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.14.2 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - body-parser@1.20.4: dependencies: bytes: 3.1.2 @@ -6771,14 +6645,6 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) - browserslist@4.28.2: - dependencies: - baseline-browser-mapping: 2.10.13 - caniuse-lite: 1.0.30001784 - electron-to-chromium: 1.5.330 - node-releases: 2.0.36 - update-browserslist-db: 1.2.3(browserslist@4.28.2) - buffer-alloc-unsafe@1.1.0: {} buffer-alloc@1.2.0: @@ -6901,8 +6767,6 @@ snapshots: caniuse-lite@1.0.30001787: {} - caniuse-lite@1.0.30001784: {} - caseless@0.12.0: {} center-align@0.1.3: @@ -7724,8 +7588,6 @@ snapshots: electron-to-chromium@1.5.334: {} - electron-to-chromium@1.5.330: {} - elliptic@6.6.1: dependencies: bn.js: 4.12.3 @@ -7841,7 +7703,7 @@ snapshots: merge2: 1.4.1 typescript: 4.9.5 - entcore@4.8.14-dev.202602171056(lodash@4.18.1): + entcore@4.8.16-develop-enabling.202602271110(lodash@4.18.1): dependencies: angular: 1.8.3 angular-route: 1.8.3 @@ -7887,10 +7749,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -8348,7 +8206,7 @@ snapshots: transitivePeerDependencies: - supports-color - follow-redirects@1.15.11: {} + follow-redirects@1.16.0: {} for-each@0.3.5: dependencies: @@ -8836,6 +8694,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + hawk@3.1.3: dependencies: boom: 2.10.1 @@ -8939,14 +8801,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - http-parser-js@0.5.10: {} http-proxy-middleware@2.0.9(@types/express@4.17.25): @@ -8964,7 +8818,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -9077,6 +8931,10 @@ snapshots: dependencies: hasown: 2.0.2 + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + is-data-descriptor@1.0.1: dependencies: hasown: 2.0.2 @@ -10119,8 +9977,6 @@ snapshots: node-releases@2.0.37: {} - node-releases@2.0.36: {} - normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -10131,8 +9987,8 @@ snapshots: normalize-package-data@3.0.3: dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.16.1 - semver: 7.7.4 + is-core-module: 2.16.2 + semver: 7.8.0 validate-npm-package-license: 3.0.4 normalize-path@2.1.1: @@ -10238,7 +10094,7 @@ snapshots: obuf@1.1.2: {} - ode-ngjs-front@1.4.23-dev.202602171058: + ode-ngjs-front@1.4.20-develop-enabling.202602061135: dependencies: chart.js: 4.4.9 core-js: 3.9.1 @@ -10246,7 +10102,7 @@ snapshots: moment: 2.29.1 ua-parser-js: 0.7.28 - ode-ts-client@1.2.5-dev.202603091504: + ode-ts-client@1.2.4-develop-enabling.202602061134: dependencies: axios: 0.21.1 core-js: 3.8.3 @@ -10825,13 +10681,6 @@ snapshots: range-parser@1.2.1: {} - raw-body@2.5.3: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - raw-body@2.5.3: dependencies: bytes: 3.1.2 @@ -11114,12 +10963,6 @@ snapshots: resolve-url@0.2.1: {} - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -11235,7 +11078,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.8.0: {} send@0.19.2: dependencies: @@ -11600,8 +11443,6 @@ snapshots: statuses@2.0.2: {} - statuses@2.0.2: {} - stream-browserify@2.0.2: dependencies: inherits: 2.0.4 @@ -11742,8 +11583,6 @@ snapshots: tapable@2.3.2: {} - tapable@2.3.2: {} - tempfile@1.1.1: dependencies: os-tmpdir: 1.0.2 @@ -11766,13 +11605,6 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - terser@5.46.1: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 - commander: 2.20.3 - source-map-support: 0.5.21 - text-extensions@1.9.0: {} textextensions@3.3.0: {} @@ -12054,12 +11886,6 @@ snapshots: upath@1.2.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -12342,8 +12168,8 @@ snapshots: dependencies: acorn: 5.7.4 acorn-dynamic-import: 2.0.2 - ajv: 6.14.0 - ajv-keywords: 3.5.2(ajv@6.14.0) + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) async: 2.6.4 enhanced-resolve: 3.4.1 escope: 3.6.0 diff --git a/pom.xml b/pom.xml index 079cda6fd4..d7317909fa 100644 --- a/pom.xml +++ b/pom.xml @@ -53,22 +53,22 @@ - 6.15.6 - 6.15.2 + 6.15-develop-enabling-SNAPSHOT + 6.15-SNAPSHOT 4.13.2 1.19.3 - 2.2.0 - 4.2.0 - 2.2.0 - 3.4.0 - 3.1.1 + 2.2-SNAPSHOT + 4.2-SNAPSHOT + 2.2-SNAPSHOT + 3.4-develop-enabling-SNAPSHOT + 3.1-SNAPSHOT 2.9.4 2.1 1.11.4 20220608.1 2.15.2 - 0.3.0 - 3.0.0 + 0.3-SNAPSHOT + 3.0-SNAPSHOT 3.0.2 0.2.0 3.9 @@ -79,6 +79,17 @@ 2.4.2 + + + + io.vertx + vertx-codegen + ${vertxVersion} + provided + + + + org.entcore @@ -209,6 +220,38 @@ + + + + maven-compiler-plugin + + + default-compile + + false + + + io.vertx.codegen.CodeGenProcessor + org.entcore.common.processor.ControllerAnnotationProcessor + + + + io.vertxvertx-codegen${vertxVersion} + + + org.entcorecommon${entCoreLibsVersion} + + + + -s + ${project.build.directory}/generated-sources/annotations + + + + + + + org.codehaus.mojo diff --git a/portal/frontend/package.json.template b/portal/frontend/package.json.template index 0d3c414176..c90ea05ad6 100644 --- a/portal/frontend/package.json.template +++ b/portal/frontend/package.json.template @@ -30,9 +30,9 @@ ] }, "dependencies": { - "@edifice.io/react": "2.5.23", - "@edifice.io/bootstrap": "2.5.23", - "@edifice.io/client": "2.5.23", + "@edifice.io/react": "%packageVersion%", + "@edifice.io/bootstrap": "%packageVersion%", + "@edifice.io/client": "%packageVersion%", "@react-spring/web": "^9.7.5", "@tanstack/react-query": "5.62.7", "@uidotdev/usehooks": "2.4.1", diff --git a/tests/src/test/js/it/scenarios/communication/_utils.ts b/tests/src/test/js/it/scenarios/communication/_utils.ts index 4ced44741d..6045f6643f 100644 --- a/tests/src/test/js/it/scenarios/communication/_utils.ts +++ b/tests/src/test/js/it/scenarios/communication/_utils.ts @@ -1,8 +1,11 @@ import { check } from "k6"; import { + getHeaders, Visible, } from "../../../node_modules/edifice-k6-commons/dist/index.js"; +import http from "k6/http"; +const rootUrl = __ENV.ROOT_URL; export function checkSearchVisible(res, expectedUserFields:string[], expectedGroupFields:string[], checkName) { const checks = {} @@ -20,4 +23,12 @@ export function checkSearchVisible(res, expectedUserFields:string[], expectedGro if(!ok) { console.error(checkName, res) } +} + +export function checkUsersCanCommunicate(user1Id: string, user2Id: string, expectedCanCommunicate: boolean) { + const res = http.get( + `${rootUrl}/communication/verify/${user1Id}/${user2Id}`, + { headers: getHeaders() }, + ); + return res.status === 200 && JSON.parse(res.body).canCommunicate === expectedCanCommunicate; } \ No newline at end of file diff --git a/tests/src/test/js/it/scenarios/communication/communication-endpoints.ts b/tests/src/test/js/it/scenarios/communication/communication-endpoints.ts new file mode 100644 index 0000000000..543abc92ba --- /dev/null +++ b/tests/src/test/js/it/scenarios/communication/communication-endpoints.ts @@ -0,0 +1,2161 @@ +import { + authenticateWeb, + initStructure, + Structure, + getUsersOfSchool, + getRandomUserWithProfile, + addCommunicationBetweenGroups, + removeCommunicationBetweenGroups, + createGroupOrFail, + Group, + addUsersToGroup, + modifyCommunicationRelationOrFail, + deleteGroupOrFail, + getProfileGroupOfStructureByType, + setDirectCommunicationOrFail, + ProfileGroup, + checkEquals, checkFalse, checkTrue, checkNotEquals, checkGte +} from "../../../node_modules/edifice-k6-commons/dist/index.js"; +import http from "k6/http"; +import {check, group} from "k6"; +import { getHeaders } from "../../../node_modules/edifice-k6-commons/dist/index.js"; + +import {checkUsersCanCommunicate} from "./_utils.ts"; + + +const rootUrl = __ENV.ROOT_URL; +const maxDuration = __ENV.MAX_DURATION || "20m"; +const schoolName = __ENV.DATA_SCHOOL_NAME || "Communication Controller"; +const gracefulStop = parseInt(__ENV.GRACEFUL_STOP || "2s"); + +const availableScenarioNames = ["testSetDirectCommunication", "testRemoveLink", "testAddLinksWithUsers", "testRemoveLinksWithUsers", "testCommuniqueWith", "testGetOutgoingRelations", "testGetIncomingRelations", "testAddLinkBetweenRelativeAndStudent", "testRemoveLinkBetweenRelativeAndStudent", "testVisibleUsers", "testVisibleGroupContains", "testSafelyAddLinksWithUsers", "testSafelyRemoveLinksWithUsers", "testAddLinkCheckOnly", "testProcessAddLinkAndChangeDirection", "testRemoveRelations", "testVerify", "testGetDiscoverVisibleUsers", "testGetDiscoverVisibleAcceptedProfile", "testGetDiscoverVisibleStructures", "testDiscoverVisibleAddCommuteUsers", "testDiscoverVisibleRemoveCommuteUsers", "testDiscoverVisibleGetGroups", "testDiscoverVisibleGetUsersInGroup", "testCreateDiscoverVisibleGroup", "testUpdateDiscoverVisibleGroup", "testAddDiscoverVisibleGroupUsers"]; + +const enabledScenarios = __ENV.COMMUNICATION_ENDPOINTS_SCENARIOS ? __ENV.COMMUNICATION_ENDPOINTS_SCENARIOS.split(",") : availableScenarioNames; + +const scenarios = enabledScenarios + .filter((name: string) => availableScenarioNames.includes(name)) + .reduce((acc: any, name: string) => { + acc[name] = { + executor: "per-vu-iterations", + exec: name, + vus: 1, + maxDuration: maxDuration, + gracefulStop, + }; + return acc; +}, {} as Record); + +export const options = { + setupTimeout: "1h", + thresholds: { + checks: ["rate == 1.00"], + }, + scenarios: scenarios +}; + +type InitData = { + structure1: Structure; + structure2: Structure; +} + +export function setup() { + let structure1: Structure; + let structure2: Structure; + + group("[Communication-Endpoints] Initialize data", () => { + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + structure1 = initStructure(`${schoolName} - School1`); + structure2 = initStructure(`${schoolName} - School2`); + }); + return { structure1 : structure1, structure2 : structure2 }; +} + +/*************************************************************************************************** + * Establish a direct communication link between two users and check that these users can communicate + * via the verify endpoint + ***************************************************************************************************/ +export function testSetDirectCommunication(data: InitData) { + group('[Endpoints] setDirectCommunication', () => { + const testName = "[setDirectCommunication]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + checkTrue(`${testName} users cannot communicate before setting direct communication`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + setDirectCommunicationOrFail(teacher1.id, teacher2.id, 'both'); + + checkTrue(`${testName} users can communicate after setting direct communication`, checkUsersCanCommunicate(teacher1.id, teacher2.id, true)); + }); +} + +/*************************************************************************************************** + * Validates the deprecated DELETE /group/:startGroupId/communique/:endGroupId endpoint. + * Creates two groups with a COMMUNIQUE link and assigns one user to each, verifies they can + * communicate, then removes the link and asserts communication is no longer possible. + ***************************************************************************************************/ +export function testRemoveLink(data: InitData) { + group('[Endpoints] removeLink', () => { + const testName = "[removeLink]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + checkTrue(`${testName} users cannot communicate before setting group communication`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + const group1: Group = createGroupOrFail(data.structure1.name + "-removeLink-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-removeLink-g2", data.structure2); + addCommunicationBetweenGroups(group1.id, group2.id); + + addUsersToGroup([teacher1.id], group1); + addUsersToGroup([teacher2.id], group2); + + checkTrue(`${testName} users can communicate after setting group communication`, checkUsersCanCommunicate(teacher1.id, teacher2.id, true)); + + // Remove the link via deprecated endpoint + const res = http.del( + `${rootUrl}/communication/group/${group1.id}/communique/${group2.id}`, + null, + { headers: getHeaders() }, + ); + checkEquals(`${testName} removeLink returns 200`, 200, res.status); + + checkTrue(`${testName} users cannot communicate after removing group communication`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates POST /group/:groupId?direction=BOTH sets the intra-group users communication direction. + * Creates a group and calls the endpoint to enable BOTH direction, then checks a 200 response. + ***************************************************************************************************/ +export function testAddLinksWithUsers(data: InitData) { + group('[Endpoints] addLinksWithUsers', () => { + const testName = "[addLinksWithUsers]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-addLinksWithUsers-g1", data.structure1); + + const res = http.post( + `${rootUrl}/communication/group/${group1.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + checkEquals(`${testName} addLinksWithUsers returns 200`, 200, res.status); + + // tear down + deleteGroupOrFail(group1); + }); +} + +/*************************************************************************************************** + * Validates DELETE /group/:groupId?direction=BOTH removes the intra-group users communication. + * Creates a group with BOTH direction enabled, then removes it and checks a 200 response. + ***************************************************************************************************/ +export function testRemoveLinksWithUsers(data: InitData) { + group('[Endpoints] removeLinksWithUsers', () => { + const testName = "[removeLinksWithUsers]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-removeLinksWithUsers-g1", data.structure1); + modifyCommunicationRelationOrFail(group1, 'both'); + + const res = http.del( + `${rootUrl}/communication/group/${group1.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + checkEquals(`${testName} removeLinksWithUsers returns 200`, 200, res.status); + + // tear down + deleteGroupOrFail(group1); + }); +} + +/*************************************************************************************************** + * Validates GET /group/:groupId returns the list of groups the source can communicate to. + * Tests: empty state for a new group, single link appears in response, multiple links listed, + * reverse direction does NOT show the source, link disappears after removal, and users in linked + * groups can actually communicate (verified via the verify endpoint). + ***************************************************************************************************/ +export function testCommuniqueWith(data: InitData) { + group('[Endpoints] communiqueWith - no link returns empty communiqueWith', () => { + const testName = "[communiqueWith-empty]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-communiqueWith-empty-g1", data.structure1); + + // A freshly created group with no outgoing link should have an empty communiqueWith + const res = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} response has id field matching the group`, group1.id, body.id); + checkTrue(`${testName} response contains communiqueWith array`, Array.isArray(body.communiqueWith)); + checkEquals(`${testName} communiqueWith is empty when no link exists`, 0, body.communiqueWith.length); + + deleteGroupOrFail(group1); + }); + + group('[Endpoints] communiqueWith - single link', () => { + const testName = "[communiqueWith-single]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-communiqueWith-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-communiqueWith-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + const res = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response has id field`, typeof body.id === "string" && body.id === group1.id); + checkTrue(`${testName} response contains communiqueWith array`, Array.isArray(body.communiqueWith)); + checkEquals(`${testName} communiqueWith has exactly 1 entry`, 1, body.communiqueWith.length); + checkTrue(`${testName} communiqueWith entry has id field`, typeof body.communiqueWith[0].id === "string"); + checkEquals(`${testName} communiqueWith contains target group`, group2.id, body.communiqueWith[0].id); + + // The reverse direction: group2 should NOT have group1 in its communiqueWith + const resReverse = http.get( + `${rootUrl}/communication/group/${group2.id}`, + { headers: getHeaders() }, + ); + const bodyReverse = JSON.parse(resReverse.body); + checkEquals(`${testName} reverse returns 200`, 200, resReverse.status); + checkFalse(`${testName} reverse communiqueWith does NOT contain source group`, bodyReverse.communiqueWith.some((g: any) => g.id === group1.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] communiqueWith - multiple links', () => { + const testName = "[communiqueWith-multi]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-communiqueWith-multi-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure1.name + "-communiqueWith-multi-g2", data.structure1); + const group3: Group = createGroupOrFail(data.structure2.name + "-communiqueWith-multi-g3", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + addCommunicationBetweenGroups(group1.id, group3.id); + + const res = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} communiqueWith has 2 entries`, 2, body.communiqueWith.length); + checkTrue(`${testName} communiqueWith contains group2`, body.communiqueWith.some((g: any) => g.id === group2.id)); + checkTrue(`${testName} communiqueWith contains group3`, body.communiqueWith.some((g: any) => g.id === group3.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(group3); + }); + + group('[Endpoints] communiqueWith - after removing link', () => { + const testName = "[communiqueWith-removed]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-communiqueWith-rm-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-communiqueWith-rm-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + // Verify link exists + const resBefore = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const bodyBefore = JSON.parse(resBefore.body); + checkTrue(`${testName} before removal contains target group`, bodyBefore.communiqueWith.some((g: any) => g.id === group2.id)); + + // Remove the link + removeCommunicationBetweenGroups(group1.id, group2.id); + + // Verify link no longer present + const resAfter = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const bodyAfter = JSON.parse(resAfter.body); + checkEquals(`${testName} after removal returns 200`, 200, resAfter.status); + checkFalse(`${testName} after removal communiqueWith does NOT contain removed group`, bodyAfter.communiqueWith.some((g: any) => g.id === group2.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] communiqueWith - users can communicate through linked groups', () => { + const testName = "[communiqueWith-users]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const group1: Group = createGroupOrFail(data.structure1.name + "-communiqueWith-vis-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-communiqueWith-vis-g2", data.structure2); + + addUsersToGroup([teacher1.id], group1); + addUsersToGroup([teacher2.id], group2); + + // Before adding link, users should not communicate + checkTrue(`${testName} users cannot communicate before link`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + // Add link and enable users communication on both groups + addCommunicationBetweenGroups(group1.id, group2.id); + + // After adding link, users should be able to communicate + checkTrue(`${testName} users can communicate after link`, checkUsersCanCommunicate(teacher1.id, teacher2.id, true)); + + // Remove the link + removeCommunicationBetweenGroups(group1.id, group2.id); + + // After removing link, users should no longer communicate + checkTrue(`${testName} users cannot communicate after link removal`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + // tear down + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates GET /group/:groupId/outgoing returns groups that receive communication from this group. + * Tests: empty array for a new group, target appears after adding a link, target does NOT list + * the source in its own outgoing, multiple outgoing links listed, and link disappears after removal. + ***************************************************************************************************/ +export function testGetOutgoingRelations(data: InitData) { + group('[Endpoints] getOutgoingRelations - empty when no link exists', () => { + const testName = "[getOutgoingRelations-empty]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-outgoing-empty-g1", data.structure1); + + const res = http.get( + `${rootUrl}/communication/group/${group1.id}/outgoing`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkEquals(`${testName} outgoing is empty`, 0, body.length); + + deleteGroupOrFail(group1); + }); + + group('[Endpoints] getOutgoingRelations - contains target after adding link', () => { + const testName = "[getOutgoingRelations-link]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-outgoing-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-outgoing-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + const res = http.get( + `${rootUrl}/communication/group/${group1.id}/outgoing`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkTrue(`${testName} outgoing contains target group`, body.some((g: any) => g.id === group2.id)); + checkTrue(`${testName} entries have id field`, body.every((g: any) => typeof g.id === "string")); + + // The target group should NOT have group1 in its outgoing + const resTarget = http.get( + `${rootUrl}/communication/group/${group2.id}/outgoing`, + { headers: getHeaders() }, + ); + const bodyTarget = JSON.parse(resTarget.body); + checkFalse(`${testName} target outgoing does NOT contain source`, bodyTarget.some((g: any) => g.id === group1.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] getOutgoingRelations - multiple outgoing links', () => { + const testName = "[getOutgoingRelations-multi]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-outgoing-multi-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure1.name + "-outgoing-multi-g2", data.structure1); + const group3: Group = createGroupOrFail(data.structure2.name + "-outgoing-multi-g3", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + addCommunicationBetweenGroups(group1.id, group3.id); + + const res = http.get( + `${rootUrl}/communication/group/${group1.id}/outgoing`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} outgoing contains group2`, body.some((g: any) => g.id === group2.id)); + checkTrue(`${testName} outgoing contains group3`, body.some((g: any) => g.id === group3.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(group3); + }); + + group('[Endpoints] getOutgoingRelations - disappears after removing link', () => { + const testName = "[getOutgoingRelations-removed]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-outgoing-rm-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-outgoing-rm-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + // Confirm present + const resBefore = http.get( + `${rootUrl}/communication/group/${group1.id}/outgoing`, + { headers: getHeaders() }, + ); + checkTrue(`${testName} before removal contains target`, JSON.parse(resBefore.body).some((g: any) => g.id === group2.id)); + + removeCommunicationBetweenGroups(group1.id, group2.id); + + const resAfter = http.get( + `${rootUrl}/communication/group/${group1.id}/outgoing`, + { headers: getHeaders() }, + ); + const bodyAfter = JSON.parse(resAfter.body); + checkEquals(`${testName} after removal returns 200`, 200, resAfter.status); + checkFalse(`${testName} after removal does NOT contain removed group`, bodyAfter.some((g: any) => g.id === group2.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates GET /group/:groupId/incoming returns groups that send communication to this group. + * Tests: empty array for a new group, source appears after adding a link, source does NOT list + * the target in its own incoming, multiple incoming links listed, and link disappears after removal. + ***************************************************************************************************/ +export function testGetIncomingRelations(data: InitData) { + group('[Endpoints] getIncomingRelations - empty when no link exists', () => { + const testName = "[getIncomingRelations-empty]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-incoming-empty-g1", data.structure1); + + const res = http.get( + `${rootUrl}/communication/group/${group1.id}/incoming`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkEquals(`${testName} incoming is empty`, 0, body.length); + + deleteGroupOrFail(group1); + }); + + group('[Endpoints] getIncomingRelations - contains source after adding link', () => { + const testName = "[getIncomingRelations-link]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-incoming-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-incoming-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + const res = http.get( + `${rootUrl}/communication/group/${group2.id}/incoming`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkTrue(`${testName} incoming contains source group`, body.some((g: any) => g.id === group1.id)); + checkTrue(`${testName} entries have id field`, body.every((g: any) => typeof g.id === "string")); + + // The source group should NOT have group2 in its incoming + const resSource = http.get( + `${rootUrl}/communication/group/${group1.id}/incoming`, + { headers: getHeaders() }, + ); + const bodySource = JSON.parse(resSource.body); + checkFalse(`${testName} source incoming does NOT contain target`, bodySource.some((g: any) => g.id === group2.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] getIncomingRelations - multiple incoming links', () => { + const testName = "[getIncomingRelations-multi]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-incoming-multi-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure1.name + "-incoming-multi-g2", data.structure1); + const group3: Group = createGroupOrFail(data.structure2.name + "-incoming-multi-g3", data.structure2); + + // Both group1 and group2 send to group3 + addCommunicationBetweenGroups(group1.id, group3.id); + addCommunicationBetweenGroups(group2.id, group3.id); + + const res = http.get( + `${rootUrl}/communication/group/${group3.id}/incoming`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} incoming contains group1`, body.some((g: any) => g.id === group1.id)); + checkTrue(`${testName} incoming contains group2`, body.some((g: any) => g.id === group2.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(group3); + }); + + group('[Endpoints] getIncomingRelations - disappears after removing link', () => { + const testName = "[getIncomingRelations-removed]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-incoming-rm-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-incoming-rm-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + removeCommunicationBetweenGroups(group1.id, group2.id); + + const res = http.get( + `${rootUrl}/communication/group/${group2.id}/incoming`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} after removal returns 200`, 200, res.status); + checkFalse(`${testName} after removal does NOT contain removed group`, body.some((g: any) => g.id === group1.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates POST /relative/:groupId?direction= creates COMMUNIQUE_RELATIVE links between a Relative + * profile group and its associated Student group. Tests directions BOTH, INCOMING, and OUTGOING, + * verifying each returns 200 with a numeric count of created relationships. + ***************************************************************************************************/ +export function testAddLinkBetweenRelativeAndStudent(data: InitData) { + group('[Endpoints] addLinkBetweenRelativeAndStudent - direction BOTH', () => { + const testName = "[addLinkRelative-both]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const relativeGroup: ProfileGroup = getProfileGroupOfStructureByType('Relative', data.structure1); + + const res = http.post( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkNotEquals(`${testName} response contains number field`, undefined, body.number); + checkEquals(`${testName} number is a number`, "number", typeof body.number); + checkGte(`${testName} number is >= 0`, 0, body.number); + }); + + group('[Endpoints] addLinkBetweenRelativeAndStudent - direction INCOMING', () => { + const testName = "[addLinkRelative-incoming]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const relativeGroup: ProfileGroup = getProfileGroupOfStructureByType('Relative', data.structure1); + + // First remove to reset state + http.del( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + + const res = http.post( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=INCOMING`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} number is a number`, "number", typeof body.number); + }); + + group('[Endpoints] addLinkBetweenRelativeAndStudent - direction OUTGOING', () => { + const testName = "[addLinkRelative-outgoing]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const relativeGroup: ProfileGroup = getProfileGroupOfStructureByType('Relative', data.structure1); + + // First remove to reset state + http.del( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + + const res = http.post( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=OUTGOING`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} number is a number`, "number", typeof body.number); + }); +} + +/*************************************************************************************************** + * Validates DELETE /relative/:groupId?direction= removes COMMUNIQUE_RELATIVE links. + * First adds links with BOTH direction, then removes them. Tests removing BOTH (full removal) + * and removing only INCOMING, verifying each returns 200 with a numeric count. + ***************************************************************************************************/ +export function testRemoveLinkBetweenRelativeAndStudent(data: InitData) { + group('[Endpoints] removeLinkBetweenRelativeAndStudent - add then remove BOTH', () => { + const testName = "[removeLinkRelative-both]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const relativeGroup: ProfileGroup = getProfileGroupOfStructureByType('Relative', data.structure1); + + // Add first + http.post( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + + const res = http.del( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + console.log("body is",body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkNotEquals(`${testName} response contains number field`, undefined, body.number); + checkEquals(`${testName} number is a number`, "number", typeof body.number); + checkGte(`${testName} number is >= 0`, 0, body.number); + }); + + group('[Endpoints] removeLinkBetweenRelativeAndStudent - remove INCOMING only', () => { + const testName = "[removeLinkRelative-incoming]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const relativeGroup: ProfileGroup = getProfileGroupOfStructureByType('Relative', data.structure1); + + // Add BOTH first, then remove only INCOMING + http.post( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=BOTH`, + null, + { headers: getHeaders() }, + ); + + const res = http.del( + `${rootUrl}/communication/relative/${relativeGroup.id}?direction=INCOMING`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} number is a number`, typeof body.number === "number"); + }); +} + +/*************************************************************************************************** + * Validates GET /visible/:userId returns the list of users visible to a given user. + * Tests: basic array response for a valid user, presence of another user in same group with BOTH + * communication enabled, and absence of a user from a different school with no comm link. + ***************************************************************************************************/ +export function testVisibleUsers(data: InitData) { + group('[Endpoints] visibleUsers - returns array for valid user', () => { + const testName = "[visibleUsers-valid]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + const res = http.get( + `${rootUrl}/communication/visible/${teacher1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + }); + + group('[Endpoints] visibleUsers - contains user in same group with comm enabled', () => { + const testName = "[visibleUsers-contains]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + + const group1: Group = createGroupOrFail(data.structure1.name + "-visUsers-g1", data.structure1); + addUsersToGroup([teacher1.id, teacher2.id], group1); + modifyCommunicationRelationOrFail(group1, 'both'); + + const res = http.get( + `${rootUrl}/communication/visible/${teacher1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} visible list contains the other teacher`, body.some((v: any) => v.id === teacher2.id)); + + // tear down + deleteGroupOrFail(group1); + }); + + group('[Endpoints] visibleUsers - does NOT contain user without comm link', () => { + const testName = "[visibleUsers-notContains]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const res = http.get( + `${rootUrl}/communication/visible/${teacher1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkFalse(`${testName} visible list does NOT contain isolated user`, body.some((v: any) => v.id === teacher2.id)); + }); +} + +/*************************************************************************************************** + * Validates GET /visible/group/:groupId returns visible users within a group for the authenticated + * user. Tests that users appear when communication is BOTH (displayName and id present) and that + * no users are visible when communication is NONE (default). + ***************************************************************************************************/ +export function testVisibleGroupContains(data: InitData) { + group('[Endpoints] visibleGroupContains - with communication BOTH', () => { + const testName = "[visibleGroupContains-both]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + + const group1: Group = createGroupOrFail(data.structure1.name + "-visGrpCont-both-g1", data.structure1); + addUsersToGroup([teacher1.id, teacher2.id], group1); + modifyCommunicationRelationOrFail(group1, 'both'); + + authenticateWeb(teacher1.login); + + const res = http.get( + `${rootUrl}/communication/visible/group/${group1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkTrue(`${testName} contains the other user`, body.some((u: any) => u.id === teacher2.id)); + checkTrue(`${testName} entries have displayName field`, body.every((u: any) => typeof u.displayName === "string")); + checkTrue(`${testName} entries have id field`, body.every((u: any) => typeof u.id === "string")); + + // tear down + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + deleteGroupOrFail(group1); + }); + + group('[Endpoints] visibleGroupContains - with communication NONE shows no users', () => { + const testName = "[visibleGroupContains-none]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const group1: Group = createGroupOrFail(data.structure1.name + "-visGrpCont-none-g1", data.structure1); + addUsersToGroup([teacher1.id, teacher2.id], group1); + // Communication is NONE by default (no modifyCommunicationRelationOrFail called) + + authenticateWeb(teacher1.login); + + const res = http.get( + `${rootUrl}/communication/visible/group/${group1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkFalse(`${testName} does NOT contain the other user when comm is NONE`, body.some((u: any) => u.id === teacher2.id)); + + // tear down + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + deleteGroupOrFail(group1); + }); +} + +/*************************************************************************************************** + * Validates POST /group/:groupId/users safely sets the intra-group "users" direction to BOTH. + * Tests: direction is returned as BOTH, users in the group can communicate afterward, and calling + * the endpoint twice is idempotent (still returns 200 with BOTH). + ***************************************************************************************************/ +export function testSafelyAddLinksWithUsers(data: InitData) { + group('[Endpoints] safelyAddLinksWithUsers - sets direction to BOTH', () => { + const testName = "[safelyAdd-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-safelyAdd-g1", data.structure1); + + const res = http.post( + `${rootUrl}/communication/group/${group1.id}/users`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response contains users field`, body.users !== undefined); + checkEquals(`${testName} users direction is BOTH`, "BOTH", body.users); + + // tear down + deleteGroupOrFail(group1); + }); + + group('[Endpoints] safelyAddLinksWithUsers - users can communicate after', () => { + const testName = "[safelyAdd-comm]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const group1: Group = createGroupOrFail(data.structure1.name + "-safelyAdd-comm-g1", data.structure1); + addUsersToGroup([teacher1.id, teacher2.id], group1); + + // Before: users should not communicate (NONE by default) + checkTrue(`${testName} cannot communicate before`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + // Add users link + http.post( + `${rootUrl}/communication/group/${group1.id}/users`, + null, + { headers: getHeaders() }, + ); + + // After: users should communicate + checkTrue(`${testName} can communicate after`, checkUsersCanCommunicate(teacher1.id, teacher2.id, true)); + + // tear down + deleteGroupOrFail(group1); + }); + + group('[Endpoints] safelyAddLinksWithUsers - idempotent (calling twice returns 200)', () => { + const testName = "[safelyAdd-idempotent]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-safelyAdd-idem-g1", data.structure1); + + http.post( + `${rootUrl}/communication/group/${group1.id}/users`, + null, + { headers: getHeaders() }, + ); + const res = http.post( + `${rootUrl}/communication/group/${group1.id}/users`, + null, + { headers: getHeaders() }, + ); + checkEquals(`${testName} second call returns 200`, 200, res.status); + checkEquals(`${testName} users still BOTH`, "BOTH", JSON.parse(res.body).users); + + // tear down + deleteGroupOrFail(group1); + }); +} + +/*************************************************************************************************** + * Validates DELETE /group/:groupId/users safely removes the intra-group "users" direction. + * Tests: basic removal returns 200, users can no longer communicate afterward, and returns 409 + * when incoming relations would conflict with removing the direction. + ***************************************************************************************************/ +export function testSafelyRemoveLinksWithUsers(data: InitData) { + group('[Endpoints] safelyRemoveLinksWithUsers - removes direction', () => { + const testName = "[safelyRemove-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-safelyRemove-g1", data.structure1); + modifyCommunicationRelationOrFail(group1, 'both'); + + const res = http.del( + `${rootUrl}/communication/group/${group1.id}/users`, + null, + { headers: getHeaders() }, + ); + checkEquals(`${testName} returns 200`, 200, res.status); + + // tear down + deleteGroupOrFail(group1); + }); + + group('[Endpoints] safelyRemoveLinksWithUsers - users can no longer communicate', () => { + const testName = "[safelyRemove-comm]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const group1: Group = createGroupOrFail(data.structure1.name + "-safelyRemove-comm-g1", data.structure1); + addUsersToGroup([teacher1.id, teacher2.id], group1); + modifyCommunicationRelationOrFail(group1, 'both'); + + // Before: users should communicate + checkTrue(`${testName} can communicate before`, checkUsersCanCommunicate(teacher1.id, teacher2.id, true)); + + // Remove users link + http.del( + `${rootUrl}/communication/group/${group1.id}/users`, + null, + { headers: getHeaders() }, + ); + + // After: users should NOT communicate + checkTrue(`${testName} cannot communicate after`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + // tear down + deleteGroupOrFail(group1); + }); + + group('[Endpoints] safelyRemoveLinksWithUsers - returns 409 when direction cannot be changed', () => { + const testName = "[safelyRemove-conflict]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-safelyRemove-conflict-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-safelyRemove-conflict-g2", data.structure2); + + // Create a link that depends on group1 having users direction + addCommunicationBetweenGroups(group2.id, group1.id); + + // group1 now has incoming relations, attempting to safely remove may result in 409 + const res = http.del( + `${rootUrl}/communication/group/${group1.id}/users`, + null, + { headers: getHeaders() }, + ); + // Either 200 (safe downgrade possible) or 409 (conflict) + checkTrue(`${testName} returns 200 or 409`, res.status === 200 || res.status === 409); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates GET /v2/group/:startGroupId/communique/:endGroupId/check simulates adding a link + * without side effects. Tests: returns 200 for new groups, confirms no link is actually created + * (communiqueWith remains empty), and returns 200 for already-linked groups. + ***************************************************************************************************/ +export function testAddLinkCheckOnly(data: InitData) { + group('[Endpoints] addLinkCheckOnly - new groups can be linked', () => { + const testName = "[checkOnly-new]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-checkOnly-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-checkOnly-g2", data.structure2); + + const res = http.get( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}/check`, + { headers: getHeaders() }, + ); + checkEquals(`${testName} returns 200`, 200, res.status); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] addLinkCheckOnly - does not actually create the link', () => { + const testName = "[checkOnly-noSideEffect]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-checkOnly-noSE-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-checkOnly-noSE-g2", data.structure2); + + // Call check + http.get( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}/check`, + { headers: getHeaders() }, + ); + + // Verify no link was created via communiqueWith + const res = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkTrue(`${testName} communiqueWith still empty after check`, + !body.communiqueWith || body.communiqueWith.length === 0 || + !body.communiqueWith.some((g: any) => g.id === group2.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] addLinkCheckOnly - already linked groups', () => { + const testName = "[checkOnly-existing]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-checkOnly-exist-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-checkOnly-exist-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + // Check on an already-linked pair + const res = http.get( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}/check`, + { headers: getHeaders() }, + ); + checkEquals(`${testName} returns 200 for already-linked groups`, 200, res.status); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates POST /v2/group/:startGroupId/communique/:endGroupId creates a COMMUNIQUE link and + * updates group directions using bitmask logic (start gets INCOMING, end gets OUTGOING). + * Tests all direction combinations: both NONE, start already OUTGOING (upgrades to BOTH), + * start already INCOMING or BOTH (no change), end already INCOMING (upgrades to BOTH), + * end already OUTGOING (no change), both already compatible (no direction change but link created), + * and verifies users in linked groups can communicate afterward. + ***************************************************************************************************/ +export function testProcessAddLinkAndChangeDirection(data: InitData) { + group('[Endpoints] processAddLinkAndChangeDirection - both NONE: start gets INCOMING, end gets OUTGOING', () => { + const testName = "[processAddLink-bothNone]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-bothNone-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-bothNone-g2", data.structure2); + + const res = http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} start direction is INCOMING`, 'INCOMING', body[group1.id]); + checkEquals(`${testName} end direction is OUTGOING`, 'OUTGOING', body[group2.id]); + + // Verify link was actually created + const verifyRes = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const verifyBody = JSON.parse(verifyRes.body); + checkTrue(`${testName} communiqueWith now contains target`, verifyBody.communiqueWith.some((g: any) => g.id === group2.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] processAddLinkAndChangeDirection - start OUTGOING: start upgrades to BOTH', () => { + const testName = "[processAddLink-startOut]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-startOut-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-startOut-g2", data.structure2); + const helper: Group = createGroupOrFail(data.structure1.name + "-processAddLink-startOut-helper", data.structure1); + + // Setup: helper→g1 gives g1 OUTGOING direction + addCommunicationBetweenGroups(helper.id, group1.id); + + // Act: g1→g2 should upgrade g1 from OUTGOING to BOTH (needs INCOMING added) + const res = http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} start direction upgraded to BOTH`, 'BOTH', body[group1.id]); + checkEquals(`${testName} end direction is OUTGOING`, 'OUTGOING', body[group2.id]); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(helper); + }); + + group('[Endpoints] processAddLinkAndChangeDirection - start INCOMING: only end changes', () => { + const testName = "[processAddLink-startIn]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-startIn-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-startIn-g2", data.structure2); + const helper: Group = createGroupOrFail(data.structure2.name + "-processAddLink-startIn-helper", data.structure2); + + // Setup: g1→helper gives g1 INCOMING direction (start of existing link) + addCommunicationBetweenGroups(group1.id, helper.id); + + // Act: g1→g2 should NOT change g1 (already has INCOMING), only g2 gets OUTGOING + const res = http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} start not in response (no change needed)`, body[group1.id] === undefined); + checkEquals(`${testName} end direction is OUTGOING`, 'OUTGOING', body[group2.id]); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(helper); + }); + + group('[Endpoints] processAddLinkAndChangeDirection - start BOTH: only end changes', () => { + const testName = "[processAddLink-startBoth]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-startBoth-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-startBoth-g2", data.structure2); + + // Setup: g1 set to BOTH via safelyAddLinksWithUsers + modifyCommunicationRelationOrFail(group1, 'both'); + + // Act: g1→g2 should NOT change g1 (BOTH already includes INCOMING), only g2 gets OUTGOING + const res = http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} start not in response (no change needed)`, body[group1.id] === undefined); + checkEquals(`${testName} end direction is OUTGOING`, 'OUTGOING', body[group2.id]); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] processAddLinkAndChangeDirection - end INCOMING: end upgrades to BOTH', () => { + const testName = "[processAddLink-endIn]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-endIn-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-endIn-g2", data.structure2); + const helper: Group = createGroupOrFail(data.structure1.name + "-processAddLink-endIn-helper", data.structure1); + + // Setup: g2→helper gives g2 INCOMING direction (g2 is start of a link) + addCommunicationBetweenGroups(group2.id, helper.id); + + // Act: g1→g2 should give g1 INCOMING, and upgrade g2 from INCOMING to BOTH (needs OUTGOING added) + const res = http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} start direction is INCOMING`, 'INCOMING', body[group1.id]); + checkEquals(`${testName} end direction upgraded to BOTH`, 'BOTH', body[group2.id]); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(helper); + }); + + group('[Endpoints] processAddLinkAndChangeDirection - end OUTGOING: only start changes', () => { + const testName = "[processAddLink-endOut]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-endOut-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-endOut-g2", data.structure2); + const helper: Group = createGroupOrFail(data.structure1.name + "-processAddLink-endOut-helper", data.structure1); + + // Setup: helper→g2 gives g2 OUTGOING direction (g2 is end of existing link) + addCommunicationBetweenGroups(helper.id, group2.id); + + // Act: g1→g2 should give g1 INCOMING, but g2 already has OUTGOING → no change for g2 + const res = http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} start direction is INCOMING`, 'INCOMING', body[group1.id]); + checkTrue(`${testName} end not in response (no change needed)`, body[group2.id] === undefined); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(helper); + }); + + group('[Endpoints] processAddLinkAndChangeDirection - both already compatible: no direction change', () => { + const testName = "[processAddLink-noChange]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-noChg-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-noChg-g2", data.structure2); + const helper1: Group = createGroupOrFail(data.structure2.name + "-processAddLink-noChg-h1", data.structure2); + const helper2: Group = createGroupOrFail(data.structure1.name + "-processAddLink-noChg-h2", data.structure1); + + // Setup: g1→helper1 gives g1 INCOMING; helper2→g2 gives g2 OUTGOING + addCommunicationBetweenGroups(group1.id, helper1.id); + addCommunicationBetweenGroups(helper2.id, group2.id); + + // Act: g1→g2 — g1 already INCOMING (no change), g2 already OUTGOING (no change) + const res = http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} start not in response (no change needed)`, body[group1.id] === undefined); + checkTrue(`${testName} end not in response (no change needed)`, body[group2.id] === undefined); + + // Verify the link was still created even though no direction changed + const verifyRes = http.get( + `${rootUrl}/communication/group/${group1.id}`, + { headers: getHeaders() }, + ); + const verifyBody = JSON.parse(verifyRes.body); + checkTrue(`${testName} communiqueWith contains target`, verifyBody.communiqueWith.some((g: any) => g.id === group2.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + deleteGroupOrFail(helper1); + deleteGroupOrFail(helper2); + }); + + group('[Endpoints] processAddLinkAndChangeDirection - users can communicate after', () => { + const testName = "[processAddLink-comm]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const group1: Group = createGroupOrFail(data.structure1.name + "-processAddLink-comm-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-processAddLink-comm-g2", data.structure2); + + addUsersToGroup([teacher1.id], group1); + addUsersToGroup([teacher2.id], group2); + + http.post( + `${rootUrl}/communication/v2/group/${group1.id}/communique/${group2.id}`, + "{}", + { headers: getHeaders("application/json") }, + ); + + checkTrue(`${testName} can communicate after`, checkUsersCanCommunicate(teacher1.id, teacher2.id, true)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates DELETE /group/:startGroupId/relations/:endGroupId removes a COMMUNIQUE link and returns + * the resulting directions of sender/receiver (null when fully downgraded). + * Tests: response contains sender/receiver with valid direction or null, link disappears from + * outgoing/incoming lists, and users can no longer communicate afterward. + ***************************************************************************************************/ +export function testRemoveRelations(data: InitData) { + group('[Endpoints] removeRelations - returns sender and receiver directions', () => { + const testName = "[removeRelations-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-removeRel-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-removeRel-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + const res = http.del( + `${rootUrl}/communication/group/${group1.id}/relations/${group2.id}`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response contains sender key`, 'sender' in body); + checkTrue(`${testName} response contains receiver key`, 'receiver' in body); + checkTrue(`${testName} sender is null or valid direction`, body.sender === null || ["NONE", "INCOMING", "OUTGOING", "BOTH"].includes(body.sender)); + checkTrue(`${testName} receiver is null or valid direction`, body.receiver === null || ["NONE", "INCOMING", "OUTGOING", "BOTH"].includes(body.receiver)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] removeRelations - link disappears from outgoing/incoming', () => { + const testName = "[removeRelations-verify]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const group1: Group = createGroupOrFail(data.structure1.name + "-removeRel-verify-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-removeRel-verify-g2", data.structure2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + // Remove + http.del( + `${rootUrl}/communication/group/${group1.id}/relations/${group2.id}`, + null, + { headers: getHeaders() }, + ); + + // Verify outgoing is empty + const resOut = http.get( + `${rootUrl}/communication/group/${group1.id}/outgoing`, + { headers: getHeaders() }, + ); + checkFalse(`${testName} outgoing no longer contains group2`, JSON.parse(resOut.body).some((g: any) => g.id === group2.id)); + + // Verify incoming is empty + const resIn = http.get( + `${rootUrl}/communication/group/${group2.id}/incoming`, + { headers: getHeaders() }, + ); + checkFalse(`${testName} incoming no longer contains group1`, JSON.parse(resIn.body).some((g: any) => g.id === group1.id)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); + + group('[Endpoints] removeRelations - users can no longer communicate', () => { + const testName = "[removeRelations-comm]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const group1: Group = createGroupOrFail(data.structure1.name + "-removeRel-comm-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-removeRel-comm-g2", data.structure2); + + addUsersToGroup([teacher1.id], group1); + addUsersToGroup([teacher2.id], group2); + addCommunicationBetweenGroups(group1.id, group2.id); + + checkTrue(`${testName} can communicate before`, checkUsersCanCommunicate(teacher1.id, teacher2.id, true)); + + http.del( + `${rootUrl}/communication/group/${group1.id}/relations/${group2.id}`, + null, + { headers: getHeaders() }, + ); + + checkTrue(`${testName} cannot communicate after`, checkUsersCanCommunicate(teacher1.id, teacher2.id, false)); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates GET /verify/:from/:to returns whether two users can communicate. + * Tests: positive case (same group with BOTH), negative case (different schools, no link), + * direct COMMUNIQUE_DIRECT link enables communication, and group-based link enables communication. + ***************************************************************************************************/ +export function testVerify(data: InitData) { + group('[Endpoints] verify - users in same group with BOTH can communicate', () => { + const testName = "[verify-canComm]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + + const group1: Group = createGroupOrFail(data.structure1.name + "-verify-can-g1", data.structure1); + addUsersToGroup([teacher1.id, teacher2.id], group1); + modifyCommunicationRelationOrFail(group1, 'both'); + + const res = http.get( + `${rootUrl}/communication/verify/${teacher1.id}/${teacher2.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response has canCommunicate field`, body.canCommunicate !== undefined); + checkTrue(`${testName} canCommunicate is boolean`, typeof body.canCommunicate === "boolean"); + checkTrue(`${testName} canCommunicate is true`, body.canCommunicate === true); + + // tear down + deleteGroupOrFail(group1); + }); + + group('[Endpoints] verify - users in different schools cannot communicate', () => { + const testName = "[verify-cannotComm]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const res = http.get( + `${rootUrl}/communication/verify/${teacher1.id}/${teacher2.id}`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkEquals(`${testName} canCommunicate is false`, false, body.canCommunicate); + }); + + group('[Endpoints] verify - direct communication link enables verify', () => { + const testName = "[verify-direct]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + // Before direct communication + const resBefore = http.get( + `${rootUrl}/communication/verify/${teacher1.id}/${teacher2.id}`, + { headers: getHeaders() }, + ); + checkEquals(`${testName} cannot communicate before`, false, JSON.parse(resBefore.body).canCommunicate); + + setDirectCommunicationOrFail(teacher1.id, teacher2.id, 'both'); + + const resAfter = http.get( + `${rootUrl}/communication/verify/${teacher1.id}/${teacher2.id}`, + { headers: getHeaders() }, + ); + checkEquals(`${testName} can communicate after direct link`, true, JSON.parse(resAfter.body).canCommunicate); + }); + + group('[Endpoints] verify - group link enables verify', () => { + const testName = "[verify-group]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + const group1: Group = createGroupOrFail(data.structure1.name + "-verify-grp-g1", data.structure1); + const group2: Group = createGroupOrFail(data.structure2.name + "-verify-grp-g2", data.structure2); + addUsersToGroup([teacher1.id], group1); + addUsersToGroup([teacher2.id], group2); + + addCommunicationBetweenGroups(group1.id, group2.id); + + const res = http.get( + `${rootUrl}/communication/verify/${teacher1.id}/${teacher2.id}`, + { headers: getHeaders() }, + ); + checkEquals(`${testName} can communicate via group link`, true, JSON.parse(res.body).canCommunicate); + + // tear down + deleteGroupOrFail(group1); + deleteGroupOrFail(group2); + }); +} + +/*************************************************************************************************** + * Validates POST /discover/visible/users returns discoverable users filtered by structure, profile, + * and search text. Tests: filtered results contain id and displayName, non-matching search returns + * empty array, and empty body still returns a valid array. + ***************************************************************************************************/ +export function testGetDiscoverVisibleUsers(data: InitData) { + group('[Endpoints] getDiscoverVisibleUsers - with structure and profile filter', () => { + const testName = "[discoverUsers-filter]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const payload = JSON.stringify({ + structures: [data.structure1.id], + profiles: ["Teacher"], + }); + const res = http.post( + `${rootUrl}/communication/discover/visible/users`, + payload, + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + if (Array.isArray(body) && body.length > 0) { + checkTrue(`${testName} entries have id field`, body.every((u: any) => typeof u.id === "string")); + checkTrue(`${testName} entries have displayName field`, body.every((u: any) => typeof u.displayName === "string")); + } + }); + + group('[Endpoints] getDiscoverVisibleUsers - with search filter', () => { + const testName = "[discoverUsers-search]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const payload = JSON.stringify({ + structures: [data.structure1.id], + profiles: ["Teacher"], + search: "zzz_nonexistent_user_zzz", + }); + const res = http.post( + `${rootUrl}/communication/discover/visible/users`, + payload, + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an empty array for non-matching search`, Array.isArray(body) && body.length === 0); + }); + + group('[Endpoints] getDiscoverVisibleUsers - empty body', () => { + const testName = "[discoverUsers-empty]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const res = http.post( + `${rootUrl}/communication/discover/visible/users`, + JSON.stringify({}), + { headers: getHeaders("application/json") }, + ); + checkEquals(`${testName} returns 200 with empty filter`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(JSON.parse(res.body))); + }); +} + +/*************************************************************************************************** + * Validates GET /discover/visible/profiles returns the profile types accepted for discover + * visibility as a string array (e.g. Teacher, Student, Relative, Personnel). + ***************************************************************************************************/ +export function testGetDiscoverVisibleAcceptedProfile(data: InitData) { + group('[Endpoints] getDiscoverVisibleAcceptedProfile - returns profiles array', () => { + const testName = "[discoverProfiles]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const res = http.get( + `${rootUrl}/communication/discover/visible/profiles`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkTrue(`${testName} entries are strings`, body.length === 0 || body.every((p: any) => typeof p === "string")); + }); +} + +/*************************************************************************************************** + * Validates GET /discover/visible/structures returns the list of structures accessible for discover + * visibility. Each entry has an id and a label field. + ***************************************************************************************************/ +export function testGetDiscoverVisibleStructures(data: InitData) { + group('[Endpoints] getDiscoverVisibleStructures - returns structures list', () => { + const testName = "[discoverStructures]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const res = http.get( + `${rootUrl}/communication/discover/visible/structures`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + if (Array.isArray(body) && body.length > 0) { + checkTrue(`${testName} entries have id field`, body.every((s: any) => typeof s.id === "string")); + checkTrue(`${testName} entries have label field`, body.every((s: any) => typeof s.label === "string")); + } + }); +} + +/*************************************************************************************************** + * Validates POST /discover/visible/add/commuting/:receiverId creates a direct COMMUNIQUE_DIRECT + * link from the authenticated user to the receiver. Tests: response contains a numeric count, + * and verifies via the verify endpoint that users can communicate afterward. + ***************************************************************************************************/ +export function testDiscoverVisibleAddCommuteUsers(data: InitData) { + group('[Endpoints] discoverVisibleAddCommuteUsers - adds commute link', () => { + const testName = "[discoverAddCommute-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + + authenticateWeb(teacher1.login); + + const res = http.post( + `${rootUrl}/communication/discover/visible/add/commuting/${teacher2.id}`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response contains count field`, body.count !== undefined); + checkTrue(`${testName} count is a number`, typeof body.count === "number"); + }); + + group('[Endpoints] discoverVisibleAddCommuteUsers - users can communicate after', () => { + const testName = "[discoverAddCommute-verify]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + http.post( + `${rootUrl}/communication/discover/visible/add/commuting/${teacher2.id}`, + null, + { headers: getHeaders() }, + ); + + // Check via verify endpoint (as admin) + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + const verifyRes = http.get( + `${rootUrl}/communication/verify/${teacher1.id}/${teacher2.id}`, + { headers: getHeaders() }, + ); + checkEquals(`${testName} users can communicate after add commute`, true, JSON.parse(verifyRes.body).canCommunicate); + }); +} + +/*************************************************************************************************** + * Validates DELETE /discover/visible/remove/commuting/:receiverId removes the direct commuting + * link. Tests: adds then removes the link verifying a numeric count response, and confirms via + * the verify endpoint that users can no longer communicate afterward. + ***************************************************************************************************/ +export function testDiscoverVisibleRemoveCommuteUsers(data: InitData) { + group('[Endpoints] discoverVisibleRemoveCommuteUsers - removes commute link', () => { + const testName = "[discoverRemoveCommute-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + + authenticateWeb(teacher1.login); + + // Add then remove + http.post( + `${rootUrl}/communication/discover/visible/add/commuting/${teacher2.id}`, + null, + { headers: getHeaders() }, + ); + + const res = http.del( + `${rootUrl}/communication/discover/visible/remove/commuting/${teacher2.id}`, + null, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response contains count field`, body.count !== undefined); + checkTrue(`${testName} count is a number`, typeof body.count === "number"); + }); + + group('[Endpoints] discoverVisibleRemoveCommuteUsers - users can no longer communicate', () => { + const testName = "[discoverRemoveCommute-verify]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const school2Users = getUsersOfSchool(data.structure2); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school2Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + // Add commute + http.post( + `${rootUrl}/communication/discover/visible/add/commuting/${teacher2.id}`, + null, + { headers: getHeaders() }, + ); + + // Remove commute + http.del( + `${rootUrl}/communication/discover/visible/remove/commuting/${teacher2.id}`, + null, + { headers: getHeaders() }, + ); + + // Verify users can no longer communicate + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + const verifyRes = http.get( + `${rootUrl}/communication/verify/${teacher1.id}/${teacher2.id}`, + { headers: getHeaders() }, + ); + checkEquals(`${testName} users cannot communicate after remove commute`, false, JSON.parse(verifyRes.body).canCommunicate); + }); +} + +/*************************************************************************************************** + * Validates GET /discover/visible/groups returns the discover groups owned by the authenticated + * user. Tests: basic array response with id fields, and a newly created group appears in the list. + ***************************************************************************************************/ +export function testDiscoverVisibleGetGroups(data: InitData) { + group('[Endpoints] discoverVisibleGetGroups - returns array', () => { + const testName = "[discoverGroups-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const res = http.get( + `${rootUrl}/communication/discover/visible/groups`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + if (Array.isArray(body) && body.length > 0) { + checkTrue(`${testName} entries have id field`, body.every((g: any) => typeof g.id === "string")); + } + }); + + group('[Endpoints] discoverVisibleGetGroups - created group appears in list', () => { + const testName = "[discoverGroups-created]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + // Create a discover group + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-discoverGroups-listed" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const createdId = JSON.parse(createRes.body).id; + + const res = http.get( + `${rootUrl}/communication/discover/visible/groups`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkTrue(`${testName} created group appears in list`, body.some((g: any) => g.id === createdId)); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); +} + +/*************************************************************************************************** + * Validates GET /discover/visible/group/:groupId/users returns users in a discover group. + * Tests: empty group returns empty array, and after adding a user via the update endpoint that + * user appears in the response. + ***************************************************************************************************/ +export function testDiscoverVisibleGetUsersInGroup(data: InitData) { + group('[Endpoints] discoverVisibleGetUsersInGroup - empty group returns empty array', () => { + const testName = "[discoverUsersInGroup-empty]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-discoverUsersInGroup-empty" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + const res = http.get( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkEquals(`${testName} group is empty`, 0, body.length); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); + + group('[Endpoints] discoverVisibleGetUsersInGroup - with users added', () => { + const testName = "[discoverUsersInGroup-withUsers]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-discoverUsersInGroup-filled" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + // Add teacher2 to the group + http.put( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + JSON.stringify({ oldUsers: [], newUsers: [teacher2.id] }), + { headers: getHeaders("application/json") }, + ); + + const res = http.get( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + { headers: getHeaders() }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response is an array`, Array.isArray(body)); + checkTrue(`${testName} contains added user`, body.some((u: any) => u.id === teacher2.id)); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); +} + +/*************************************************************************************************** + * Validates POST /discover/visible/group creates a new discover group owned by the authenticated + * user. Tests: response contains a non-empty string id, empty name is rejected (non-200), and the + * created group is retrievable via the groups list endpoint. + ***************************************************************************************************/ +export function testCreateDiscoverVisibleGroup(data: InitData) { + group('[Endpoints] createDiscoverVisibleGroup - returns id', () => { + const testName = "[createDiscoverGroup-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const payload = JSON.stringify({ name: "test-createDiscoverGroup-basic" }); + const res = http.post( + `${rootUrl}/communication/discover/visible/group`, + payload, + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response contains id`, body.id !== undefined); + checkTrue(`${testName} id is a string`, typeof body.id === "string"); + checkTrue(`${testName} id is non-empty`, body.id.length > 0); + }); + + group('[Endpoints] createDiscoverVisibleGroup - invalid name rejected', () => { + const testName = "[createDiscoverGroup-invalid]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + // Empty name should fail + const res = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "" }), + { headers: getHeaders("application/json") }, + ); + checkTrue(`${testName} empty name returns error (not 200)`, res.status !== 200); + }); + + group('[Endpoints] createDiscoverVisibleGroup - created group is retrievable', () => { + const testName = "[createDiscoverGroup-retrieve]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-createDiscoverGroup-retrieve" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + // Verify it appears in the groups list + const listRes = http.get( + `${rootUrl}/communication/discover/visible/groups`, + { headers: getHeaders() }, + ); + const groups = JSON.parse(listRes.body); + checkTrue(`${testName} created group appears in groups list`, groups.some((g: any) => g.id === groupId)); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); +} + +/*************************************************************************************************** + * Validates PUT /discover/visible/group/:groupId renames an existing discover group. + * Tests: successful rename returns 200 with the same group id, and empty name is rejected. + ***************************************************************************************************/ +export function testUpdateDiscoverVisibleGroup(data: InitData) { + group('[Endpoints] updateDiscoverVisibleGroup - renames the group', () => { + const testName = "[updateDiscoverGroup-rename]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-updateDiscoverGroup-original" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + const payload = JSON.stringify({ name: "test-updateDiscoverGroup-renamed" }); + const res = http.put( + `${rootUrl}/communication/discover/visible/group/${groupId}`, + payload, + { headers: getHeaders("application/json") }, + ); + const body = JSON.parse(res.body); + checkEquals(`${testName} returns 200`, 200, res.status); + checkTrue(`${testName} response contains id`, body.id !== undefined); + checkEquals(`${testName} id matches original`, groupId, body.id); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); + + group('[Endpoints] updateDiscoverVisibleGroup - empty name rejected', () => { + const testName = "[updateDiscoverGroup-invalid]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-updateDiscoverGroup-toInvalid" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + const res = http.put( + `${rootUrl}/communication/discover/visible/group/${groupId}`, + JSON.stringify({ name: "" }), + { headers: getHeaders("application/json") }, + ); + checkTrue(`${testName} empty name returns error (not 200)`, res.status !== 200); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); +} + +/*************************************************************************************************** + * Validates PUT /discover/visible/group/:groupId/users updates the user list of a discover group. + * Tests: adding a user makes them appear in the group, empty newUsers is rejected (non-200), and + * replacing users (oldUsers removed, newUsers added) works correctly. + ***************************************************************************************************/ +export function testAddDiscoverVisibleGroupUsers(data: InitData) { + group('[Endpoints] addDiscoverVisibleGroupUsers - adds users to group', () => { + const testName = "[addDiscoverGroupUsers-basic]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-addDiscoverGroupUsers-basic" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + const payload = JSON.stringify({ + oldUsers: [], + newUsers: [teacher2.id], + }); + const res = http.put( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + payload, + { headers: getHeaders("application/json") }, + ); + checkEquals(`${testName} returns 200`, 200, res.status); + + // Verify user is in group + const usersRes = http.get( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + { headers: getHeaders() }, + ); + const usersBody = JSON.parse(usersRes.body); + checkTrue(`${testName} user appears in group after adding`, usersBody.some((u: any) => u.id === teacher2.id)); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); + + group('[Endpoints] addDiscoverVisibleGroupUsers - empty newUsers rejected', () => { + const testName = "[addDiscoverGroupUsers-emptyUsers]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-addDiscoverGroupUsers-empty" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + const payload = JSON.stringify({ + oldUsers: [], + newUsers: [], + }); + const res = http.put( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + payload, + { headers: getHeaders("application/json") }, + ); + checkTrue(`${testName} empty newUsers returns error (not 200)`, res.status !== 200); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); + + group('[Endpoints] addDiscoverVisibleGroupUsers - replaces users (old removed, new added)', () => { + const testName = "[addDiscoverGroupUsers-replace]"; + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const school1Users = getUsersOfSchool(data.structure1); + const teacher1 = getRandomUserWithProfile(school1Users, 'Teacher'); + const teacher2 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1]); + const teacher3 = getRandomUserWithProfile(school1Users, 'Teacher', [teacher1, teacher2]); + + authenticateWeb(teacher1.login); + + const createRes = http.post( + `${rootUrl}/communication/discover/visible/group`, + JSON.stringify({ name: "test-addDiscoverGroupUsers-replace" }), + { headers: getHeaders("application/json") }, + ); + + if (createRes.status === 200) { + const groupId = JSON.parse(createRes.body).id; + + // First add teacher2 + http.put( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + JSON.stringify({ oldUsers: [], newUsers: [teacher2.id] }), + { headers: getHeaders("application/json") }, + ); + + // Now replace teacher2 with teacher3 + const res = http.put( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + JSON.stringify({ oldUsers: [teacher2.id], newUsers: [teacher3.id] }), + { headers: getHeaders("application/json") }, + ); + checkEquals(`${testName} replace returns 200`, 200, res.status); + + // Verify teacher3 is in group and teacher2 is not + const usersRes = http.get( + `${rootUrl}/communication/discover/visible/group/${groupId}/users`, + { headers: getHeaders() }, + ); + const usersBody = JSON.parse(usersRes.body); + checkTrue(`${testName} new user is in group`, usersBody.some((u: any) => u.id === teacher3.id)); + checkFalse(`${testName} old user is NOT in group`, usersBody.some((u: any) => u.id === teacher2.id)); + } else { + checkTrue(`${testName} discover feature responded (may not be enabled)`, createRes.status !== 0); + } + }); +} \ No newline at end of file diff --git a/tests/src/test/js/it/scenarios/directory/directory-endpoints.ts b/tests/src/test/js/it/scenarios/directory/directory-endpoints.ts index a65f44613f..898986dc3f 100644 --- a/tests/src/test/js/it/scenarios/directory/directory-endpoints.ts +++ b/tests/src/test/js/it/scenarios/directory/directory-endpoints.ts @@ -29,7 +29,7 @@ import { addUsersToGroup } from "../../../node_modules/edifice-k6-commons/dist/index.js"; import http from "k6/http"; -import {check, group} from "k6"; +import {check, fail, group} from "k6"; const maxDuration = __ENV.MAX_DURATION || "20m"; @@ -46,7 +46,7 @@ export const options = { checks: ["rate == 1.00"], }, scenarios: { - /*testClassEndpoints: { + testClassEndpoints: { executor: "per-vu-iterations", exec: "testClassEndpoints", vus: 1, @@ -60,7 +60,6 @@ export const options = { maxDuration: maxDuration, gracefulStop, }, - */ testUserEndpoints: { executor: "per-vu-iterations", exec: "testUserEndpoints", @@ -75,20 +74,6 @@ export const options = { maxDuration: maxDuration, gracefulStop, }, - testStructureEndpoints: { - executor: "per-vu-iterations", - exec: "testStructureEndpoints", - vus: 1, - maxDuration: maxDuration, - gracefulStop, - }, - testClassEndpoints: { - executor: "per-vu-iterations", - exec: "testClassEndpoints", - vus: 1, - maxDuration: maxDuration, - gracefulStop, - }, testShareBookmarkEndpoints: { executor: "per-vu-iterations", exec: "testShareBookmarkEndpoints", @@ -350,13 +335,85 @@ export function testUserEndpoints(data: InitData) { } }); + // TODO add AAF structure initialization otherwise the test won't pass group('[Directory] GET /duplicates - List duplicates', () => { authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); - const res = http.get(`${rootUrl}/directory/duplicates?structure=${data.structure.id}`, { headers: getHeaders() }); + + let res = http.get(`${rootUrl}/directory/duplicates?structure=${data.structure.id}`, { headers: getHeaders() }); check(res, { 'list duplicates returns 200': (r) => r.status === 200, 'list duplicates is array': (r) => Array.isArray(JSON.parse(r.body)), }); + + const resInherit = http.get(`${rootUrl}/directory/duplicates?structure=${data.structure.id}&inherit=true`, { headers: getHeaders() }); + check(resInherit, { + 'list duplicates (inherit) returns 200': (r) => r.status === 200, + 'list duplicates (inherit) is array': (r) => Array.isArray(JSON.parse(r.body)), + }); + + const users = getUsersOfSchool(data.structure); + const teacher = getRandomUserWithProfile(users, 'Teacher'); + authenticateWeb(teacher.login); + const resForbidden = http.get(`${rootUrl}/directory/duplicates?structure=${data.structure.id}`, { headers: getHeaders() }); + check(resForbidden, { + 'list duplicates as non-ADML is denied (401)': (r) => r.status === 401, + }); + // restore the ADMC session + authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); + + const victim = users.find((u) => (u.source === 'AAF' || u.source === 'AAF1D') && u.type === 'Teacher') + || users.find((u) => u.source === 'AAF' || u.source === 'AAF1D'); + + if (!victim) { + fail('[Directory] GET /duplicates - no AAF/AAF1D user found, skipping end-to-end duplicate detection'); + } else { + const victimRes = http.get(`${rootUrl}/directory/user/${victim.id}`, { headers: getHeaders() }); + const victimInfo = JSON.parse(victimRes.body); + + const twin = createUserAndGetData({ + firstName: victimInfo.firstName, + lastName: victimInfo.lastName, + type: victim.type, + structureId: data.structure.id, + birthDate: victimInfo.birthDate, + positionIds: [], + }); + + const markRes = http.post(`${rootUrl}/directory/duplicates/mark`, null, { headers: getHeaders() }); + check(markRes, { + 'mark duplicates returns 200': (r) => r.status === 200, + }); + + const matchesPair = (d: any) => + (d.user1.id === victim.id && d.user2.id === twin.id) || + (d.user1.id === twin.id && d.user2.id === victim.id); + res = http.get(`${rootUrl}/directory/duplicates?structure=${data.structure.id}`, { headers: getHeaders() }); + let body = JSON.parse(res.body); + check(res, { + 'list duplicates after mark returns 200': (r) => r.status === 200, + 'list duplicates after mark is not empty': () => Array.isArray(body) && body.length > 0, + 'list duplicates contains the created pair': () => Array.isArray(body) && body.some(matchesPair), + 'created duplicate has a score >= 5': () => Array.isArray(body) && body.filter(matchesPair).every((d: any) => d.score >= 5), + 'duplicate entries are well-formed': () => Array.isArray(body) && body.every((d: any) => + typeof d.score === 'number' && + !!d.user1 && !!d.user1.id && !!d.user1.firstName && !!d.user1.lastName && + !!d.user2 && !!d.user2.id && !!d.user2.firstName && !!d.user2.lastName), + }); + + + const ignoreRes = http.del(`${rootUrl}/directory/duplicate/ignore/${victim.id}/${twin.id}`, null, { headers: getHeaders() }); + check(ignoreRes, { + 'ignore duplicate returns 200': (r) => r.status === 200, + }); + + res = http.get(`${rootUrl}/directory/duplicates?structure=${data.structure.id}`, { headers: getHeaders() }); + body = JSON.parse(res.body); + check(res, { + 'duplicate pair is gone after ignore': () => Array.isArray(body) && !body.some(matchesPair), + }); + + http.del(`${rootUrl}/directory/user?userId=${twin.id}`, null, { headers: getHeaders() }); + } }); group('[Directory] GET /list/isolated - List isolated users', () => { authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD); diff --git a/tests/src/test/js/pnpm-lock.yaml b/tests/src/test/js/pnpm-lock.yaml index e2f6689a76..278e7d04ea 100644 --- a/tests/src/test/js/pnpm-lock.yaml +++ b/tests/src/test/js/pnpm-lock.yaml @@ -12,20 +12,20 @@ importers: specifier: ^0.54.2 version: 0.54.2 edifice-k6-commons: - specifier: 2.1.6-develop-b2school-3 - version: 2.1.6-develop-b2school-3 + specifier: develop + version: 2.1.14-develop.2 packages: '@types/k6@0.54.2': resolution: {integrity: sha512-B5LPxeQm97JnUTpoKNE1UX9jFp+JiJCAXgZOa2P7aChxVoPQXKfWMzK+739xHq3lPkKj1aV+HeOxkP56g/oWBg==} - edifice-k6-commons@2.1.6-develop-b2school-3: - resolution: {integrity: sha512-H4qJMdtIdR025qWy45/T5IsN2SJU1N/GmLXiSqXfbt96rylJoeA7YysZLJAU6vW3kwJBHYjenn5rwCHWkwVMlA==} + edifice-k6-commons@2.1.14-develop.2: + resolution: {integrity: sha512-smomxKkRG/2A9nN5UVz6c0QKl7vTj/V/DQEQnZYA1l2QANOegVFQcEP74LvoBcqsXjDMvSdx7n6g7SsUpIkXpg==} engines: {node: '>=18'} snapshots: '@types/k6@0.54.2': {} - edifice-k6-commons@2.1.6-develop-b2school-3: {} + edifice-k6-commons@2.1.14-develop.2: {} diff --git a/timeline/src/main/java/org/entcore/timeline/services/impl/PeriodicTimelineMailerService.java b/timeline/src/main/java/org/entcore/timeline/services/impl/PeriodicTimelineMailerService.java index 0198107328..eae2827b98 100644 --- a/timeline/src/main/java/org/entcore/timeline/services/impl/PeriodicTimelineMailerService.java +++ b/timeline/src/main/java/org/entcore/timeline/services/impl/PeriodicTimelineMailerService.java @@ -798,15 +798,15 @@ private Future applyDailyRuleToNotification(NotificationCon if (notificationPreference == null){ continue; } - notificationPreference.getJsonObject("preferences", new JsonObject()) + JsonObject userNotificationPref = notificationPreference.getJsonObject("preferences", new JsonObject()) .getJsonObject("config", new JsonObject()) .getJsonObject(notificationName, new JsonObject()); if (TimelineNotificationsLoader.Frequencies.DAILY.name().equals( - notificationPrefsMixin("defaultFrequency", notificationPreference, notificationsDefaults.getJsonObject(notificationName))) && + notificationPrefsMixin("defaultFrequency", userNotificationPref, notificationsDefaults.getJsonObject(notificationName))) && !TimelineNotificationsLoader.Restrictions.INTERNAL.name().equals( - notificationPrefsMixin("restriction", notificationPreference, notificationsDefaults.getJsonObject(notificationName))) && + notificationPrefsMixin("restriction", userNotificationPref, notificationsDefaults.getJsonObject(notificationName))) && !TimelineNotificationsLoader.Restrictions.HIDDEN.name().equals( - notificationPrefsMixin("restriction", notificationPreference, notificationsDefaults.getJsonObject(notificationName)))) { + notificationPrefsMixin("restriction", userNotificationPref, notificationsDefaults.getJsonObject(notificationName)))) { notification.put("template", notificationsDefaults.getJsonObject(notificationName, new JsonObject()).getString("template", ""));