diff --git a/.github/workflows/anchore-analysis.yml b/.github/workflows/anchore-analysis.yml new file mode 100644 index 0000000000..4dd5be97f8 --- /dev/null +++ b/.github/workflows/anchore-analysis.yml @@ -0,0 +1,36 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow checks out code, builds an image, performs a container image +# vulnerability scan with Anchore's Grype tool, and integrates the results with GitHub Advanced Security +# code scanning feature. For more information on the Anchore scan action usage +# and parameters, see https://github.com/anchore/scan-action. For more +# information on Anchore's container image scanning tool Grype, see +# https://github.com/anchore/grype +name: Anchore Container Scan + +on: + push: + branches: [ develop] + workflow_dispatch: + +jobs: + Anchore-Build-Scan: + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v2 + - name: Build the Docker image + run: docker pull lfeoperatorfabric/of-cards-consultation-business-service:3.2.0.RELEASE + - name: Run the Anchore scan action itself with GitHub Advanced Security code scanning integration enabled + uses: anchore/scan-action@v3 + with: + image: "lfeoperatorfabric/of-cards-consultation-business-service:3.2.0.RELEASE" + acs-report-enable: true + fail-build: false + - name: Upload Anchore Scan Report + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: results.sarif diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93ca8e97d0..e4ce63993a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,6 +48,7 @@ on: jobs: build: runs-on: ubuntu-latest + if: ${{ github.event.inputs.dockerPush != 'true' && github.event.inputs.dockerPushLatest != 'true' && github.event.inputs.doc != 'true' && github.event.inputs.docLatest != 'true' && github.event_name != 'schedule' && github.ref_name != 'master' }} steps: - uses: actions/checkout@v2 @@ -94,7 +95,6 @@ jobs: if: ${{ github.event.inputs.build == 'true' || github.event_name == 'schedule' || github.event_name == 'pull_request' || github.event_name == 'push'}} run: | export OF_VERSION=$( $HOME/.sdkman/etc/config ; + echo sdkman_auto_selfupdate=true >> $HOME/.sdkman/etc/config ; + source $HOME/.sdkman/bin/sdkman-init.sh; + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + source ./bin/load_environment_light.sh; + sudo apt-get install jq + echo "npm version $(npm -version)" + echo "node version $(node --version)" + sdk version + javac -version + git config --global user.email "opfabtech@gmail.com" + git config --global user.name "OpfabTech" + + - name: Build + if: ${{ github.event.inputs.build == 'true' || github.event_name == 'schedule' || github.event_name == 'pull_request' || github.event_name == 'push'}} + run: | + export OF_VERSION=$( updateSubscriptionAndPublish(Mono updateSubscriptionAndPublish(Mono fetchOldCards(CardSubscription subscription,Instant publishFrom,Instant start,Instant end) { - subscription.updateCurrentUserWithPerimeters(); + return fetchOldCards0(publishFrom, start, end, subscription.getCurrentUserWithPerimeters()); } diff --git a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/repositories/CardCustomRepositoryImpl.java b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/repositories/CardCustomRepositoryImpl.java index 732e269b3c..0be7fded17 100644 --- a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/repositories/CardCustomRepositoryImpl.java +++ b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/repositories/CardCustomRepositoryImpl.java @@ -121,18 +121,24 @@ private Criteria computeCriteriaForProcessesStatesNotNotified(CurrentUserWithPer private Criteria getCriteriaForRange(Instant rangeStart,Instant rangeEnd) { - if (rangeStart==null) return where(END_DATE_FIELD).lte(rangeEnd); + if (rangeStart==null) return new Criteria().orOperator( + where(END_DATE_FIELD).lte(rangeEnd), + where(PUBLISH_DATE_FIELD).lte(rangeEnd) + ); if (rangeEnd==null) return new Criteria().orOperator( where(END_DATE_FIELD).gte(rangeStart), - where(START_DATE_FIELD).gte(rangeStart) + where(START_DATE_FIELD).gte(rangeStart), + where(PUBLISH_DATE_FIELD).gte(rangeStart) ); return new Criteria().orOperator( where(START_DATE_FIELD).gte(rangeStart).lte(rangeEnd), where(END_DATE_FIELD).gte(rangeStart).lte(rangeEnd), new Criteria().andOperator( where(START_DATE_FIELD).lte(rangeStart), - where(END_DATE_FIELD).gte(rangeEnd)) - ); + where(END_DATE_FIELD).gte(rangeEnd) + ), + where(PUBLISH_DATE_FIELD).gte(rangeStart).lte(rangeEnd) + ); } private Criteria publishDateCriteria(Instant publishFrom) { diff --git a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardRoutingUtilities.java b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardRoutingUtilities.java new file mode 100644 index 0000000000..216925d3a9 --- /dev/null +++ b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardRoutingUtilities.java @@ -0,0 +1,143 @@ +/* Copyright (c) 2021, RTE (http://www.rte-france.com) + * See AUTHORS.txt + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 + * This file is part of the OperatorFabric project. + */ + +package org.opfab.cards.consultation.services; + + +import lombok.extern.slf4j.Slf4j; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import org.opfab.users.model.CurrentUserWithPerimeters; +import org.opfab.users.model.RightsEnum; + + +import java.util.*; + +@Slf4j +public class CardRoutingUtilities { + + public static final String GROUPS_SUFFIX = "Groups"; + public static final String DELETE_OPERATION = "DELETE"; + public static final String ERROR_MESSAGE_PARSING = "ERROR during received message parsing"; + + + private CardRoutingUtilities(){ + } + + public static String createDeleteCardMessageForUserNotRecipient(JSONObject cardOperation,String userLogin) { + + String typeOperation = (cardOperation.get("type") != null) ? (String) cardOperation.get("type") : ""; + + if (typeOperation.equals("ADD") || typeOperation.equals("UPDATE")) { + JSONObject cardObj = (JSONObject) cardOperation.get("card"); + + String idCard = (cardObj != null) ? (String) cardObj.get("id") : ""; + + log.debug("Send delete card with id {} for user {}", idCard, userLogin); + cardOperation.replace("type", DELETE_OPERATION); + cardOperation.put("cardId", idCard); + + return cardOperation.toJSONString(); + } + + return ""; + } + + + public static boolean checkIfUserMustBeNotifiedForThisProcessState(String process, String state, CurrentUserWithPerimeters currentUserWithPerimeters) { + Map> processesStatesNotNotified = currentUserWithPerimeters.getProcessesStatesNotNotified(); + return ! ((processesStatesNotNotified != null) && (processesStatesNotNotified.get(process) != null) && + (processesStatesNotNotified.get(process).contains(state))); + } + + private static Map loadUserRightsPerProcessAndState(CurrentUserWithPerimeters currentUserWithPerimeters) { + Map userRightsPerProcessAndState = new HashMap<>(); + if (currentUserWithPerimeters.getComputedPerimeters() != null) + currentUserWithPerimeters.getComputedPerimeters() + .forEach(perimeter -> userRightsPerProcessAndState.put(perimeter.getProcess() + "." + perimeter.getState(), perimeter.getRights())); + return userRightsPerProcessAndState; + } + + private static boolean isReceiveRightsForProcessAndState(String processId, String stateId, Map userRightsPerProcessAndState) { + final RightsEnum rights = userRightsPerProcessAndState.get(processId + '.' + stateId); + return rights == RightsEnum.RECEIVE || rights == RightsEnum.RECEIVEANDWRITE; + } + + /** Rules for receiving cards : + 1) If the card is sent to user1, the card is received and visible for user1 if he has the receive right for the + corresponding process/state (Receive or ReceiveAndWrite) + 2) If the card is sent to GROUP1 (or ENTITY1), the card is received and visible for user if all of the following is true : + - he's a member of GROUP1 (or ENTITY1) + - he has the receive right for the corresponding process/state (Receive or ReceiveAndWrite) + 3) If the card is sent to ENTITY1 and GROUP1, the card is received and visible for user if all of the following is true : + - he's a member of ENTITY1 (either directly or through one of its children entities) + - he's a member of GROUP1 + - he has the receive right for the corresponding process/state (Receive or ReceiveAndWrite) + **/ + public static boolean checkIfUserMustReceiveTheCard(JSONObject cardOperation, CurrentUserWithPerimeters currentUserWithPerimeters) { + Map userRightsPerProcessAndState = loadUserRightsPerProcessAndState(currentUserWithPerimeters); + + JSONArray groupRecipientsIdsArray = (JSONArray) cardOperation.get("groupRecipientsIds"); + JSONArray entityRecipientsIdsArray = (JSONArray) cardOperation.get("entityRecipientsIds"); + JSONArray userRecipientsIdsArray = (JSONArray) cardOperation.get("userRecipientsIds"); + JSONObject cardObj = (JSONObject) cardOperation.get("card"); + String typeOperation = (cardOperation.get("type") != null) ? (String) cardOperation.get("type") : ""; + + String idCard = null; + String process = ""; + String state = ""; + if (cardObj != null) { + idCard = (cardObj.get("id") != null) ? (String) cardObj.get("id") : ""; + process = (String) cardObj.get("process"); + state = (String) cardObj.get("state"); + } + + if (!checkIfUserMustBeNotifiedForThisProcessState(process, state, currentUserWithPerimeters)) + return false; + + String processStateKey = process + "." + state; + List userGroups = currentUserWithPerimeters.getUserData().getGroups(); + List userEntities = currentUserWithPerimeters.getUserData().getEntities(); + + log.debug("Check if user {} shall receive card {} for processStateKey {}", currentUserWithPerimeters.getUserData().getLogin(), idCard, processStateKey); + + // First, we check if the user has the right for receiving this card (Receive or ReceiveAndWrite) + if ((!typeOperation.equals(DELETE_OPERATION)) && (!isReceiveRightsForProcessAndState(process, state, userRightsPerProcessAndState))) + return false; + + // Now, we check if the user is member of the group and/or entity (or the recipient himself) + if (checkInCaseOfCardSentToUserDirectly(userRecipientsIdsArray,currentUserWithPerimeters.getUserData().getLogin())) { // user only + log.debug("User {} is in user recipients and shall receive card {}", currentUserWithPerimeters.getUserData().getLogin(), idCard); + return true; + } + + if (checkInCaseOfCardSentToGroupOrEntityOrBoth(userGroups, groupRecipientsIdsArray, userEntities, entityRecipientsIdsArray)) { + log.debug("User {} is member of a group or/and entity that shall receive card {}", currentUserWithPerimeters.getUserData().getLogin(), idCard); + return true; + } + return false; + } + + + private static boolean checkInCaseOfCardSentToUserDirectly(JSONArray userRecipientsIdsArray,String userLogin) { + return (userRecipientsIdsArray != null && !Collections.disjoint(Arrays.asList(userLogin), userRecipientsIdsArray)); + } + + private static boolean checkInCaseOfCardSentToGroupOrEntityOrBoth(List userGroups, JSONArray groupRecipientsIdsArray, + List userEntities, JSONArray entityRecipientsIdsArray) { + if ((groupRecipientsIdsArray != null) && (!groupRecipientsIdsArray.isEmpty()) + && (Collections.disjoint(userGroups, groupRecipientsIdsArray))) + return false; + if ((entityRecipientsIdsArray != null) && (!entityRecipientsIdsArray.isEmpty()) + && (Collections.disjoint(userEntities, entityRecipientsIdsArray))) + return false; + return ! ((groupRecipientsIdsArray == null || groupRecipientsIdsArray.isEmpty()) && + (entityRecipientsIdsArray == null || entityRecipientsIdsArray.isEmpty())); + } +} diff --git a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscription.java b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscription.java index dccc216797..59ff7bb1fc 100644 --- a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscription.java +++ b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscription.java @@ -15,106 +15,46 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import net.minidev.json.JSONArray; -import net.minidev.json.JSONObject; -import net.minidev.json.parser.JSONParser; -import net.minidev.json.parser.ParseException; import org.opfab.springtools.configuration.oauth.UserServiceCache; import org.opfab.users.model.CurrentUserWithPerimeters; -import org.opfab.users.model.RightsEnum; -import org.opfab.utilities.AmqpUtils; -import org.springframework.amqp.core.*; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; - import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; -import java.time.Instant; -import java.util.*; -/** - *

This object manages subscription to AMQP exchange

- * - *

Two exchanges are used, {@link #cardExchange} and {@link #userExchange}. - * See amqp.xml resource file ([project]/services/cards-publication/src/main/resources/amqp.xml) - * for their exact configuration

- * - * - */ @Slf4j @EqualsAndHashCode public class CardSubscription { - public static final String GROUPS_SUFFIX = "Groups"; - public static final String PROCESS_SUFFIX = "Process"; - public static final String USER_SUFFIX = "User"; - public static final String DELETE_OPERATION = "DELETE"; - public static final String ERROR_MESSAGE_PARSING = "ERROR during received message parsing"; - private String cardsQueueName; - private String processQueueName; - private String userQueueName; - private long current = 0; - @Getter + private CurrentUserWithPerimeters currentUserWithPerimeters; @Getter private String id; @Getter private Flux publisher; - private Flux amqpPublisher; private FluxSink messageSink; - private AmqpAdmin amqpAdmin; - private FanoutExchange cardExchange; - private FanoutExchange processExchange; - private FanoutExchange userExchange; - private ConnectionFactory connectionFactory; - private MessageListenerContainer cardListener; - private MessageListenerContainer processListener; - private MessageListenerContainer userListener; - @Getter - private Instant startingPublishDate; - @Getter - private boolean cleared = false; - private final String clientId; - private String userLogin; - private boolean fluxHasBeenFirstInit = false; + private String userLogin; protected UserServiceCache userServiceCache; - - /** * Constructs a card subscription and init access to AMQP exchanges */ @Builder public CardSubscription(CurrentUserWithPerimeters currentUserWithPerimeters, - String clientId, - AmqpAdmin amqpAdmin, - FanoutExchange cardExchange, - FanoutExchange processExchange, - FanoutExchange userExchange, - ConnectionFactory connectionFactory) { + String clientId + ) { userLogin = currentUserWithPerimeters.getUserData().getLogin(); this.id = computeSubscriptionId(userLogin, clientId); this.currentUserWithPerimeters = currentUserWithPerimeters; - this.amqpAdmin = amqpAdmin; - this.cardExchange = cardExchange; - this.processExchange = processExchange; - this.userExchange = userExchange; - this.connectionFactory = connectionFactory; - this.clientId = clientId; - this.cardsQueueName = computeSubscriptionId(userLogin + GROUPS_SUFFIX, this.clientId); - this.processQueueName = computeSubscriptionId(userLogin + PROCESS_SUFFIX, this.clientId); - this.userQueueName = computeSubscriptionId(userLogin + USER_SUFFIX, this.clientId); + } public String getUserLogin() { return userLogin; } - - public void updateCurrentUserWithPerimeters() { + + public CurrentUserWithPerimeters getCurrentUserWithPerimeters() { if (userServiceCache != null) try { currentUserWithPerimeters = userServiceCache.fetchCurrentUserWithPerimetersFromCacheOrProxy(userLogin); @@ -130,243 +70,41 @@ public void updateCurrentUserWithPerimeters() { // log.info("Cannot get new perimeter for user {} , use old one ", userLogin); } + return currentUserWithPerimeters; } - + public static String computeSubscriptionId(String prefix, String clientId) { return prefix + "#" + clientId; } - public void initSubscription(int retries, long retryInterval, Runnable doOnCancel) { - AmqpUtils.createQueue(amqpAdmin, cardsQueueName, cardExchange, retries, retryInterval); - AmqpUtils.createQueue(amqpAdmin, processQueueName, processExchange, retries, retryInterval); - AmqpUtils.createQueue(amqpAdmin, userQueueName, userExchange, retries, retryInterval); - this.cardListener = createMessageListenerContainer(cardsQueueName); - this.processListener = createMessageListenerContainer(processQueueName); - this.userListener = createMessageListenerContainer(userQueueName); + public void initSubscription(Runnable doOnCancel) { this.publisher = Flux.create(emitter -> { - log.debug("Create message flux for user {}", userLogin); + log.info("Create subscription for user {}", userLogin); this.messageSink = emitter; - registerListener(cardListener); - registerProcessListener(processListener); - registerUserListener(userListener); - emitter.onRequest(v -> log.debug("STARTING subscription for user {}", userLogin)); + emitter.onRequest(v -> log.debug("Starting subscription for user {}", userLogin)); emitter.onDispose(() -> { - log.debug("DISPOSING subscription for user {}", userLogin); + log.info("Disposing subscription for user {}", userLogin); doOnCancel.run(); }); - - if (!this.fluxHasBeenFirstInit) { - emitter.next("INIT"); - this.fluxHasBeenFirstInit = true; - } else - emitter.next("RESTORE"); - cardListener.start(); - processListener.start(); - userListener.start(); + emitter.next("INIT"); }); } - private void registerListener(MessageListenerContainer mlc) { - mlc.setupMessageListener(message -> { - JSONObject card; - try - { - card = (JSONObject) (new JSONParser(JSONParser.MODE_PERMISSIVE)).parse(message.getBody()); - } - catch(ParseException e){ log.error(ERROR_MESSAGE_PARSING, e); return;} - - if (checkIfUserMustReceiveTheCard(card)){ - publishDataIntoSubscription(new String(message.getBody())); - } - // In case of ADD or UPDATE, we send a delete card operation (to delete the card from the feed, more information in OC-297) - else { - String deleteMessage = createDeleteCardMessageForUserNotRecipient(card); - if (! deleteMessage.isEmpty()) publishDataIntoSubscription(deleteMessage); - } - }); - } - - private void registerProcessListener(MessageListenerContainer mlc) { - mlc.setupMessageListener(message -> publishDataIntoSubscription(new String(message.getBody()))); - } - - private void registerUserListener(MessageListenerContainer mlc) { - mlc.setupMessageListener(message -> { - String modifiedUserLogin = new String(message.getBody()); - - if (this.userLogin.equals(modifiedUserLogin)) - publishDataIntoSubscription("USER_CONFIG_CHANGE"); - }); - } - - /** - * Stops associated {@link MessageListenerContainer} and delete queues - */ - public void clearSubscription() { - log.debug("Clear subscription for user {}", userLogin); - cardListener.stop(); - amqpAdmin.deleteQueue(cardsQueueName); - processListener.stop(); - amqpAdmin.deleteQueue(processQueueName); - userListener.stop(); - amqpAdmin.deleteQueue(userQueueName); - cleared = true; - } - - /** - * - * @return true if associated AMQP listeners are still running - */ - public boolean checkActive() { - return cardListener == null || cardListener.isRunning(); - } - - - /** - * Create a {@link MessageListenerContainer} for the specified queue - * @param queueName AMQP queue name - * @return listener container for the specified queue - */ - public MessageListenerContainer createMessageListenerContainer(String queueName) { - - SimpleMessageListenerContainer mlc = new SimpleMessageListenerContainer(connectionFactory); - mlc.addQueueNames(queueName); - mlc.setAcknowledgeMode(AcknowledgeMode.AUTO); - return mlc; - } - - - public void updateRange() { - startingPublishDate = Instant.now(); - } - - public void publishDataIntoSubscription(String message) { - this.messageSink.next(message); + if (this.messageSink!=null) this.messageSink.next(message); } - public void publishDataFluxIntoSubscription(Flux messageFlux) { messageFlux.subscribe(next -> this.messageSink.next(next)); } - public String createDeleteCardMessageForUserNotRecipient(JSONObject cardOperation) { - String typeOperation = (cardOperation.get("type") != null) ? (String) cardOperation.get("type") : ""; - if (typeOperation.equals("ADD") || typeOperation.equals("UPDATE")) { - JSONObject cardObj = (JSONObject) cardOperation.get("card"); - - String idCard = (cardObj != null) ? (String) cardObj.get("id") : ""; - - log.debug("Send delete card with id {} for user {}", idCard, userLogin); - cardOperation.replace("type", DELETE_OPERATION); - cardOperation.put("cardId", idCard); - - return cardOperation.toJSONString(); - } - - return ""; - } - - public boolean checkIfUserMustReceiveTheCard(JSONObject cardOperation) { - updateCurrentUserWithPerimeters(); - return checkIfUserMustReceiveTheCard(cardOperation, currentUserWithPerimeters); - } - - public boolean checkIfUserMustBeNotifiedForThisProcessState(String process, String state, CurrentUserWithPerimeters currentUserWithPerimeters) { - Map> processesStatesNotNotified = currentUserWithPerimeters.getProcessesStatesNotNotified(); - return ! ((processesStatesNotNotified != null) && (processesStatesNotNotified.get(process) != null) && - (((List)processesStatesNotNotified.get(process)).contains(state))); - } + - private Map loadUserRightsPerProcessAndState() { - Map userRightsPerProcessAndState = new HashMap<>(); - if (currentUserWithPerimeters.getComputedPerimeters() != null) - currentUserWithPerimeters.getComputedPerimeters() - .forEach(perimeter -> userRightsPerProcessAndState.put(perimeter.getProcess() + "." + perimeter.getState(), perimeter.getRights())); - return userRightsPerProcessAndState; - } - - private boolean isReceiveRightsForProcessAndState(String processId, String stateId, Map userRightsPerProcessAndState) { - final RightsEnum rights = userRightsPerProcessAndState.get(processId + '.' + stateId); - return rights == RightsEnum.RECEIVE || rights == RightsEnum.RECEIVEANDWRITE; - } - - /** Rules for receiving cards : - 1) If the card is sent to user1, the card is received and visible for user1 if he has the receive right for the - corresponding process/state (Receive or ReceiveAndWrite) - 2) If the card is sent to GROUP1 (or ENTITY1), the card is received and visible for user if all of the following is true : - - he's a member of GROUP1 (or ENTITY1) - - he has the receive right for the corresponding process/state (Receive or ReceiveAndWrite) - 3) If the card is sent to ENTITY1 and GROUP1, the card is received and visible for user if all of the following is true : - - he's a member of ENTITY1 (either directly or through one of its children entities) - - he's a member of GROUP1 - - he has the receive right for the corresponding process/state (Receive or ReceiveAndWrite) - **/ - public boolean checkIfUserMustReceiveTheCard(JSONObject cardOperation, CurrentUserWithPerimeters currentUserWithPerimeters) { - Map userRightsPerProcessAndState = loadUserRightsPerProcessAndState(); - - JSONArray groupRecipientsIdsArray = (JSONArray) cardOperation.get("groupRecipientsIds"); - JSONArray entityRecipientsIdsArray = (JSONArray) cardOperation.get("entityRecipientsIds"); - JSONArray userRecipientsIdsArray = (JSONArray) cardOperation.get("userRecipientsIds"); - JSONObject cardObj = (JSONObject) cardOperation.get("card"); - String typeOperation = (cardOperation.get("type") != null) ? (String) cardOperation.get("type") : ""; - - String idCard = null; - String process = ""; - String state = ""; - if (cardObj != null) { - idCard = (cardObj.get("id") != null) ? (String) cardObj.get("id") : ""; - process = (String) cardObj.get("process"); - state = (String) cardObj.get("state"); - } - - if (!checkIfUserMustBeNotifiedForThisProcessState(process, state, currentUserWithPerimeters)) - return false; - - String processStateKey = process + "." + state; - List userGroups = currentUserWithPerimeters.getUserData().getGroups(); - List userEntities = currentUserWithPerimeters.getUserData().getEntities(); - - log.debug("Check if user {} shall receive card {} for processStateKey {}", userLogin, idCard, processStateKey); - - // First, we check if the user has the right for receiving this card (Receive or ReceiveAndWrite) - if ((!typeOperation.equals(DELETE_OPERATION)) && (!isReceiveRightsForProcessAndState(process, state, userRightsPerProcessAndState))) - return false; - - // Now, we check if the user is member of the group and/or entity (or the recipient himself) - if (checkInCaseOfCardSentToUserDirectly(userRecipientsIdsArray)) { // user only - log.debug("User {} is in user recipients and shall receive card {}", userLogin, idCard); - return true; - } - - if (checkInCaseOfCardSentToGroupOrEntityOrBoth(userGroups, groupRecipientsIdsArray, userEntities, entityRecipientsIdsArray)) { - log.debug("User {} is member of a group or/and entity that shall receive card {}", userLogin, idCard); - return true; - } - return false; - } - - - boolean checkInCaseOfCardSentToUserDirectly(JSONArray userRecipientsIdsArray) { - return (userRecipientsIdsArray != null && !Collections.disjoint(Arrays.asList(userLogin), userRecipientsIdsArray)); - } - - private boolean checkInCaseOfCardSentToGroupOrEntityOrBoth(List userGroups, JSONArray groupRecipientsIdsArray, - List userEntities, JSONArray entityRecipientsIdsArray) { - if ((groupRecipientsIdsArray != null) && (!groupRecipientsIdsArray.isEmpty()) - && (Collections.disjoint(userGroups, groupRecipientsIdsArray))) - return false; - if ((entityRecipientsIdsArray != null) && (!entityRecipientsIdsArray.isEmpty()) - && (Collections.disjoint(userEntities, entityRecipientsIdsArray))) - return false; - return ! ((groupRecipientsIdsArray == null || groupRecipientsIdsArray.isEmpty()) && - (entityRecipientsIdsArray == null || entityRecipientsIdsArray.isEmpty())); - } } diff --git a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscriptionService.java b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscriptionService.java index 8af87d58f6..fa9f8c5b84 100644 --- a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscriptionService.java +++ b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/CardSubscriptionService.java @@ -10,74 +10,39 @@ package org.opfab.cards.consultation.services; import lombok.extern.slf4j.Slf4j; +import net.minidev.json.parser.JSONParser; + import org.opfab.springtools.configuration.oauth.UserServiceCache; import org.opfab.users.model.CurrentUserWithPerimeters; -import org.springframework.amqp.core.AmqpAdmin; -import org.springframework.amqp.core.FanoutExchange; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.stereotype.Service; - import java.util.Collection; -import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledFuture; - -/** - *

Centralize request for generating {@link CardSubscription} and deleting them.

- * - *

Uses a {@link ThreadPoolTaskScheduler} delay definitive deletion of subscription (defaults to 10s)

- * - * - */ +import net.minidev.json.JSONObject; +import net.minidev.json.parser.ParseException; + @Service @Slf4j public class CardSubscriptionService { - private final ThreadPoolTaskScheduler taskScheduler; - private final FanoutExchange cardExchange; - private final FanoutExchange processExchange; - private final FanoutExchange userExchange; - private final AmqpAdmin amqpAdmin; - private final long deletionDelay; + + private static final String ERROR_MESSAGE_PARSING = "ERROR during received message parsing"; private final long heartbeatDelay; - private final ConnectionFactory connectionFactory; private Map cache = new ConcurrentHashMap<>(); - private Map> pendingEvict = new ConcurrentHashMap<>(); @Autowired protected UserServiceCache userServiceCache; - @Value("${operatorfabric.amqp.connectionRetries:10}") - private int retries; - - @Value("${operatorfabric.amqp.connectionRetryInterval:5000}") - private long retryInterval; - - @Autowired - public CardSubscriptionService(ThreadPoolTaskScheduler taskScheduler, - FanoutExchange cardExchange, - FanoutExchange processExchange, - FanoutExchange userExchange, - ConnectionFactory connectionFactory, - AmqpAdmin amqpAdmin, - @Value("${operatorfabric.subscriptiondeletion.delay:10000}") - long deletionDelay, + public CardSubscriptionService( @Value("${operatorfabric.heartbeat.delay:10000}") long heartbeatDelay) { - this.cardExchange = cardExchange; - this.processExchange = processExchange; - this.userExchange = userExchange; - this.taskScheduler = taskScheduler; - this.amqpAdmin = amqpAdmin; - this.connectionFactory = connectionFactory; - this.deletionDelay = deletionDelay; + this.heartbeatDelay = heartbeatDelay; Thread heartbeat = new Thread(){ + @Override public void run(){ sendHeartbeatMessageInAllSubscriptions(); } @@ -103,116 +68,42 @@ private void sendHeartbeatMessageInAllSubscriptions() } log.debug("Send heartbeat to all subscription"); cache.keySet().forEach(key -> { - log.debug("Send heartbeat to {}",key); - sendHeartbeat(cache.get(key)); + CardSubscription sub = cache.get(key); + if (sub!=null) // subscription can be null if it has been evict during the process of going throw the keys + { + log.debug("Send heartbeat to {}",key); + sub.publishDataIntoSubscription("HEARTBEAT"); + } }); } } - private void sendHeartbeat(CardSubscription subscription) - { - if (subscription!=null) subscription.publishDataIntoSubscription("HEARTBEAT"); - } + + public CardSubscription subscribe( CurrentUserWithPerimeters currentUserWithPerimeters,String clientId) { - /** - *

Generates a {@link CardSubscription} or retrieve it from a local {@link CardSubscription} cache.

- *

If it finds a {@link CardSubscription} from cache, it will try to cancel possible scheduled evict

- */ - public synchronized CardSubscription subscribe( - CurrentUserWithPerimeters currentUserWithPerimeters, - String clientId) { - String subId = CardSubscription.computeSubscriptionId(currentUserWithPerimeters.getUserData().getLogin(), clientId); - CardSubscription cardSubscription = cache.get(subId); - // The builder may seem declare a bit to early but it allows usage in both branch of the later condition + String subId = CardSubscription.computeSubscriptionId(currentUserWithPerimeters.getUserData().getLogin(),clientId); CardSubscription.CardSubscriptionBuilder cardSubscriptionBuilder = CardSubscription.builder() - .currentUserWithPerimeters(currentUserWithPerimeters) - .clientId(clientId) - .amqpAdmin(amqpAdmin) - .cardExchange(this.cardExchange) - .processExchange(this.processExchange) - .userExchange(this.userExchange) - .connectionFactory(this.connectionFactory); - if (cardSubscription == null) { - cardSubscription = buildSubscription(subId, cardSubscriptionBuilder); - } else { - if (!cancelEviction(subId)) { - cardSubscription = cache.get(subId); - if (cardSubscription == null) { - cardSubscription = buildSubscription(subId, cardSubscriptionBuilder); - } - } - } - return cardSubscription; - } - - private CardSubscription buildSubscription(String subId, CardSubscription.CardSubscriptionBuilder cardSubscriptionBuilder) { + .currentUserWithPerimeters(currentUserWithPerimeters) + .clientId(clientId); CardSubscription cardSubscription; cardSubscription = cardSubscriptionBuilder.build(); - cardSubscription.initSubscription(retries, retryInterval, () -> scheduleEviction(subId)); + cardSubscription.initSubscription(() -> evictSubscription(subId)); cache.put(subId, cardSubscription); - log.debug("Subscription created with id {}", cardSubscription.getId()); + log.info("Subscription created with id {} for user {} ", cardSubscription.getId(), cardSubscription.getUserLogin()); cardSubscription.userServiceCache = this.userServiceCache; return cardSubscription; } - /** - * Schedule deletion of subscription in deletionDelay millis - * - * @param subId - * Subscription computed id - */ - public void scheduleEviction(String subId) { - if (!pendingEvict.containsKey(subId)) { - ScheduledFuture scheduled = taskScheduler.schedule(createEvictTask(subId), - new Date(System.currentTimeMillis() + deletionDelay)); - pendingEvict.put(subId, scheduled); - log.debug("Eviction scheduled for id {}", subId); - } - } - - /** - * Cancel scheduled evict if any - * - * @param subId - * subscription auto-generated id - * @return true if eviction was successfully cancelled, false may indicate that either no cancellation was - * possible or no eviction was previously scheduled - */ - public synchronized boolean cancelEviction(String subId) { - ScheduledFuture scheduled = pendingEvict.get(subId); - if (scheduled != null) { - boolean canceled = scheduled.cancel(false); - pendingEvict.remove(subId); - log.debug("Eviction canceled with id {}", subId); - return canceled; + public void evictSubscription(String subId) { + + CardSubscription sub = cache.get(subId); + if (sub==null) { + log.info("Subscription with id {} already evicted , as it is not existing anymore ", subId); + return; } - return false; - } - - /** - * Evict subscription definitively - * - * @param subId subscription unique id - * subscription autogenerated id - */ - public synchronized void evict(String subId) { - log.debug("Trying to evict subscription with id {}", subId); - cache.get(subId).clearSubscription(); - cache.remove(subId); - pendingEvict.remove(subId); - log.debug("Subscription with id {} evicted ", subId); - } - - /** - * Create a runnable to to launch {@link #evict(String)} - * - * @param subId - * subscription autogenerated id - * @return the generated task - */ - private Runnable createEvictTask(String subId) { - return () -> evict(subId); + cache.remove(subId); + log.info("Subscription with id {} evicted (user {})", subId , sub.getUserLogin()); } /** @@ -231,14 +122,58 @@ public CardSubscription findSubscription(CurrentUserWithPerimeters currentUserWi return cache.get(subId); } + // only use for testing purpose public void clearSubscriptions() { - this.cache.forEach((k,v)->v.clearSubscription()); this.cache.clear(); - this.pendingEvict.clear(); } - public Collection getSubscriptions() - { + public Collection getSubscriptions() { return cache.values(); } + + + public void onMessage(String queueName, String message) { + + log.debug("receive from rabbit queue {} message {}",queueName,message); + switch (queueName) { + case "process": + cache.values().forEach(subscription -> subscription.publishDataIntoSubscription(message)); + break; + case "user": + cache.values().forEach(subscription -> { + if (message.equals(subscription.getUserLogin())) + subscription.publishDataIntoSubscription("USER_CONFIG_CHANGE"); + }); + break; + case "card": + cache.values().forEach(subscription -> processNewCard(message,subscription)); + break; + default: + log.info("unrecognized queue {}" , queueName); + } + } + + private void processNewCard(String cardAsString, CardSubscription subscription) { + JSONObject card; + try { + card = (JSONObject) (new JSONParser(JSONParser.MODE_PERMISSIVE)).parse(cardAsString); + } catch (ParseException e) { + log.error(ERROR_MESSAGE_PARSING, e); + return; + } + + if (CardRoutingUtilities.checkIfUserMustReceiveTheCard(card, + subscription.getCurrentUserWithPerimeters())) { + subscription.publishDataIntoSubscription(cardAsString); + } + // In case of ADD or UPDATE, we send a delete card operation (to delete the card + // from the feed, more information in OC-297) + else { + String deleteMessage = CardRoutingUtilities.createDeleteCardMessageForUserNotRecipient(card, + subscription.getUserLogin()); + if (!deleteMessage.isEmpty()) + subscription.publishDataIntoSubscription(deleteMessage); + } + } + } diff --git a/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/RabbitMQEntryPoint.java b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/RabbitMQEntryPoint.java new file mode 100644 index 0000000000..af1aa5e00d --- /dev/null +++ b/services/cards-consultation/src/main/java/org/opfab/cards/consultation/services/RabbitMQEntryPoint.java @@ -0,0 +1,104 @@ +/* Copyright (c) 2021, RTE (http://www.rte-france.com) + * See AUTHORS.txt + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 + * This file is part of the OperatorFabric project. + */ + +package org.opfab.cards.consultation.services; + +import javax.annotation.PreDestroy; + +import org.opfab.utilities.AmqpUtils; +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.FanoutExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class RabbitMQEntryPoint { + + private static final String CARD_QUEUE_NAME = "card"; + private static final String PROCESS_QUEUE_NAME = "process"; + private static final String USER_QUEUE_NAME = "user"; + + @Value("${operatorfabric.amqp.connectionRetries:30}") + private int retries; + + @Value("${operatorfabric.amqp.connectionRetryInterval:5000}") + private long retryInterval; + + private AmqpAdmin amqpAdmin; + private FanoutExchange cardExchange; + private FanoutExchange processExchange; + private FanoutExchange userExchange; + private ConnectionFactory connectionFactory; + private CardSubscriptionService cardSubscriptionService; + + private SimpleMessageListenerContainer cardListener; + private SimpleMessageListenerContainer userListener; + private SimpleMessageListenerContainer processListener; + + + @Autowired + public RabbitMQEntryPoint(AmqpAdmin amqpAdmin, + FanoutExchange cardExchange, + FanoutExchange processExchange, + FanoutExchange userExchange, + ConnectionFactory connectionFactory, + CardSubscriptionService cardSubscriptionService) { + this.amqpAdmin = amqpAdmin; + this.cardExchange = cardExchange; + this.processExchange = processExchange; + this.userExchange = userExchange; + this.connectionFactory = connectionFactory; + this.cardSubscriptionService = cardSubscriptionService; + + log.info("Starting rabbitMQ queues"); + createQueues(); + + } + + private void createQueues() { + AmqpUtils.createQueue(amqpAdmin, CARD_QUEUE_NAME, cardExchange, retries, retryInterval); + AmqpUtils.createQueue(amqpAdmin, PROCESS_QUEUE_NAME, processExchange, retries, retryInterval); + AmqpUtils.createQueue(amqpAdmin, USER_QUEUE_NAME, userExchange, retries, retryInterval); + cardListener = startListener(CARD_QUEUE_NAME); + userListener = startListener(USER_QUEUE_NAME); + processListener = startListener(PROCESS_QUEUE_NAME); + } + + private SimpleMessageListenerContainer startListener(String queueName) { + SimpleMessageListenerContainer mlc = new SimpleMessageListenerContainer(connectionFactory); + mlc.addQueueNames(queueName); + mlc.setAcknowledgeMode(AcknowledgeMode.AUTO); + mlc.setupMessageListener( + message -> cardSubscriptionService.onMessage(queueName, new String(message.getBody()))); + mlc.start(); + return mlc; + } + + @PreDestroy + public void destroy() { + + cardListener.stop(); + userListener.stop(); + processListener.stop(); + + // we just stop the listener but + // we do not delete queue via amqpAdmin.deleteQueue(...) + // as we want to keep the message received while the service is down + + log.debug("******* Rabbit Listener stopped "); + + } +} diff --git a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/controllers/CardOperationsControllerShould.java b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/controllers/CardOperationsControllerShould.java index e5b9cc2b20..c720555f32 100644 --- a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/controllers/CardOperationsControllerShould.java +++ b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/controllers/CardOperationsControllerShould.java @@ -179,7 +179,7 @@ private void initCardData() { //create later published cards in past //this one overrides first - StepVerifier.create(repository.save(createSimpleCard(1, nowPlusOne, nowMinusTwo, nowMinusOne, "operator3", new String[]{"rte","operator"}, null))) + StepVerifier.create(repository.save(createSimpleCard(1, nowMinusOne, nowMinusTwo, nowMinusOne, "operator3", new String[]{"rte","operator"}, null))) .expectNextCount(1) .expectComplete() .verify(); @@ -214,7 +214,7 @@ void receiveOlderCards() { StepVerifier.FirstStep verifier = StepVerifier .create(publisher.map(s -> TestUtilities.readCardOperation(mapper, s)) .doOnNext(TestUtilities::logCardOperation)); - for (int i = 0; i < 7; i++) + for (int i = 0; i < 8; i++) verifier.assertNext(op -> { assertThat(op.getCard()).isNotNull(); results.put(op.getCard().getProcessInstanceId(), op); @@ -227,6 +227,7 @@ void receiveOlderCards() { CardOperation card5 = (CardOperation) results.get("PROCESS5"); CardOperation card6 = (CardOperation) results.get("PROCESS6"); CardOperation card8 = (CardOperation) results.get("PROCESS8"); + CardOperation card9 = (CardOperation) results.get("PROCESS9"); CardOperation card10 = (CardOperation) results.get("PROCESS10"); assertThat(card2.getCard().getId()).isEqualTo("PROCESS.PROCESS2"); @@ -241,6 +242,8 @@ void receiveOlderCards() { assertThat(card6.getPublishDate()).isEqualTo(nowMinusThree); assertThat(card8.getCard().getId()).isEqualTo("PROCESS.PROCESS8"); assertThat(card8.getPublishDate()).isEqualTo(nowMinusThree); + assertThat(card9.getCard().getId()).isEqualTo("PROCESS.PROCESS9"); + assertThat(card9.getPublishDate()).isEqualTo(nowPlusOne); assertThat(card10.getCard().getId()).isEqualTo("PROCESS.PROCESS10"); assertThat(card10.getPublishDate()).isEqualTo(nowPlusOne); diff --git a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/repositories/CardRepositoryShould.java b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/repositories/CardRepositoryShould.java index 509174ea1b..4e738139ce 100644 --- a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/repositories/CardRepositoryShould.java +++ b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/repositories/CardRepositoryShould.java @@ -184,9 +184,9 @@ void getZeroCardInRange() @Test void getTwoCardsInRange() { - persistCard(createSimpleCard("1", now, now, nowPlusOne, LOGIN,null, null)); - persistCard(createSimpleCard("2", now, now, nowPlusTwo, LOGIN, null,null)); - persistCard(createSimpleCard("3", now, nowPlusTwo, nowPlusThree, LOGIN,null,null)); + persistCard(createSimpleCard("1", nowMinusOne, now, nowPlusOne, LOGIN,null, null)); + persistCard(createSimpleCard("2", nowMinusOne, now, nowPlusTwo, LOGIN, null,null)); + persistCard(createSimpleCard("3", nowMinusOne, nowPlusTwo, nowPlusThree, LOGIN,null,null)); StepVerifier.create(repository.getCardOperations(null, now,nowPlusOne, adminUser) .doOnNext(TestUtilities::logCardOperation)) @@ -230,11 +230,11 @@ void getThreeCardsInRange() } @Test - void getTwoCardsInRangeWitnNoEnd() + void getTwoCardsInRangeWithNoEnd() { - persistCard(createSimpleCard("1", now, nowMinusOne, nowPlusThree, LOGIN,null, null)); - persistCard(createSimpleCard("2", now, nowMinusOne, null, LOGIN, null,null)); - persistCard(createSimpleCard("3", now, nowPlusOne, null, LOGIN,null,null)); + persistCard(createSimpleCard("1", nowMinusOne, nowMinusOne, nowPlusThree, LOGIN,null, null)); + persistCard(createSimpleCard("2", nowMinusOne, nowMinusOne, null, LOGIN, null,null)); + persistCard(createSimpleCard("3", nowMinusOne, nowPlusOne, null, LOGIN,null,null)); HashMap results = new HashMap(); StepVerifier.create(repository.getCardOperations(null, now,nowPlusTwo, adminUser) @@ -255,6 +255,60 @@ void getTwoCardsInRangeWitnNoEnd() assertCard(card2, "PROCESS.PROCESS3", "PUBLISHER", "0"); } + + @Test + void getTwoCardsInRangeWithoutStart() + { + persistCard(createSimpleCard("1", now, now, nowPlusOne, LOGIN,null, null)); + persistCard(createSimpleCard("2", nowMinusTwo, now, nowPlusOne, LOGIN, null,null)); + persistCard(createSimpleCard("3", now, nowMinusTwo, nowMinusOne, LOGIN,null,null)); + + HashMap results = new HashMap(); + StepVerifier.create(repository.getCardOperations(null, null, nowMinusOne, adminUser) + .doOnNext(TestUtilities::logCardOperation)) + .assertNext(op -> { + assertThat(op.getCard()).isNotNull(); + results.put(op.getCard().getProcessInstanceId(), op); + }) + .assertNext(op -> { + assertThat(op.getCard()).isNotNull(); + results.put(op.getCard().getProcessInstanceId(), op); + }) + .expectComplete() + .verify(); + CardOperation card1 = (CardOperation) results.get("PROCESS2"); + CardOperation card2 = (CardOperation) results.get("PROCESS3"); + assertCard(card1, "PROCESS.PROCESS2", "PUBLISHER", "0"); + assertCard(card2, "PROCESS.PROCESS3", "PUBLISHER", "0"); + + } + + @Test + void getTwoCardsInRangeWithoutEnd() + { + persistCard(createSimpleCard("1", nowMinusOne, nowMinusOne, now, LOGIN,null, null)); + persistCard(createSimpleCard("2", now, nowPlusOne, nowPlusTwo, LOGIN, null,null)); + persistCard(createSimpleCard("3", nowPlusOne, nowMinusTwo, nowMinusOne, LOGIN,null,null)); + + HashMap results = new HashMap(); + StepVerifier.create(repository.getCardOperations(null, nowPlusOne, null, adminUser) + .doOnNext(TestUtilities::logCardOperation)) + .assertNext(op -> { + assertThat(op.getCard()).isNotNull(); + results.put(op.getCard().getProcessInstanceId(), op); + }) + .assertNext(op -> { + assertThat(op.getCard()).isNotNull(); + results.put(op.getCard().getProcessInstanceId(), op); + }) + .expectComplete() + .verify(); + CardOperation card1 = (CardOperation) results.get("PROCESS2"); + CardOperation card2 = (CardOperation) results.get("PROCESS3"); + assertCard(card1, "PROCESS.PROCESS2", "PUBLISHER", "0"); + assertCard(card2, "PROCESS.PROCESS3", "PUBLISHER", "0"); + + } @Test void getZeroCardAfterPublishDate() @@ -292,11 +346,11 @@ void getTwoCardsAfterPublishDate() } @Test - void getOneCardInRangeAndAfterPublishDate () + void getOneCardInRangeAndAfterPublishDate() { persistCard(createSimpleCard("1", now, now, nowPlusOne, LOGIN,null, null)); - persistCard(createSimpleCard("2", nowPlusTwo, now, nowPlusOne, LOGIN, null,null)); - persistCard(createSimpleCard("3", nowPlusTwo, nowPlusTwo, nowPlusThree, LOGIN,null,null)); + persistCard(createSimpleCard("2", nowPlusOne, now, nowPlusOne, LOGIN, null,null)); + persistCard(createSimpleCard("3", nowPlusOne, nowPlusTwo, nowPlusThree, LOGIN,null,null)); StepVerifier.create(repository.getCardOperations(nowPlusOne, nowPlusTwo,nowPlusThree, adminUser) .doOnNext(TestUtilities::logCardOperation)) @@ -309,6 +363,22 @@ void getOneCardInRangeAndAfterPublishDate () } + @Test + void getOneCardWithPublishDateInRange() + { + persistCard(createSimpleCard("1", nowMinusOne, nowPlusTwo, nowPlusThree, LOGIN,null, null)); + persistCard(createSimpleCard("2", now, nowPlusTwo, nowPlusThree, LOGIN, null,null)); + persistCard(createSimpleCard("3", nowPlusTwo, nowPlusTwo, nowPlusThree, LOGIN,null,null)); + + StepVerifier.create(repository.getCardOperations(nowMinusThree, now,nowPlusOne, adminUser) + .doOnNext(TestUtilities::logCardOperation)) + .assertNext(op -> { + assertThat(op.getCard()).isNotNull(); + assertCard(op,"PROCESS.PROCESS2", "PUBLISHER", "0"); + }) + .expectComplete() + .verify(); + } @Test void getNoCardAsRteUserEntity1IsNotAdminUser() { diff --git a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/routes/ConnectionRoutesShould.java b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/routes/ConnectionRoutesShould.java index b43f24508f..a0e8b09e8a 100644 --- a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/routes/ConnectionRoutesShould.java +++ b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/routes/ConnectionRoutesShould.java @@ -11,23 +11,17 @@ import lombok.extern.slf4j.Slf4j; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.AfterEach; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.opfab.cards.consultation.application.IntegrationTestApplication; import org.opfab.cards.consultation.configuration.webflux.ConnectionRoutesConfig; -import org.opfab.cards.consultation.model.ArchivedCardConsultationData; -import org.opfab.cards.consultation.model.ConnectionData; -import org.opfab.cards.consultation.repositories.ArchivedCardRepository; import org.opfab.cards.consultation.services.CardSubscription; import org.opfab.cards.consultation.services.CardSubscriptionService; import org.opfab.springtools.configuration.test.UserServiceCacheTestApplication; import org.opfab.springtools.configuration.test.WithMockOpFabUserReactive; -import org.opfab.test.EmptyListComparator; import org.opfab.users.model.CurrentUserWithPerimeters; import org.opfab.users.model.User; import org.springframework.beans.factory.annotation.Autowired; @@ -38,13 +32,8 @@ import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.test.StepVerifier; - -import java.lang.reflect.Array; -import java.time.Instant; - import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; + @ExtendWith(SpringExtension.class) @SpringBootTest(classes = { IntegrationTestApplication.class, ConnectionRoutesConfig.class, @@ -100,7 +89,6 @@ void respondWithNoUSerConnectedIsEmpty() { void respondWithOneUserConnected() { CardSubscription subscription = service.subscribe(currentUserWithPerimeters, "test"); subscription.getPublisher().subscribe(log::info); - Assertions.assertThat(subscription.checkActive()).isTrue(); webTestClient.get().uri("/connections").exchange().expectStatus().isOk() .expectBody() .jsonPath("$[0].login").isEqualTo("testuser"); diff --git a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/services/CardRoutingUtilitiesShould.java b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/services/CardRoutingUtilitiesShould.java new file mode 100644 index 0000000000..e2fad9cd5b --- /dev/null +++ b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/services/CardRoutingUtilitiesShould.java @@ -0,0 +1,178 @@ +/* Copyright (c) 2018-2021, RTE (http://www.rte-france.com) + * See AUTHORS.txt + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 + * This file is part of the OperatorFabric project. + */ + +package org.opfab.cards.consultation.services; + +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import net.minidev.json.JSONObject; +import net.minidev.json.parser.JSONParser; +import net.minidev.json.parser.ParseException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.opfab.cards.consultation.application.IntegrationTestApplication; +import org.opfab.users.model.ComputedPerimeter; +import org.opfab.users.model.CurrentUserWithPerimeters; +import org.opfab.users.model.RightsEnum; +import org.opfab.users.model.User; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = {IntegrationTestApplication.class}) +@Slf4j +@ActiveProfiles("test") +class CardRoutingUtilitiesShould { + + private CurrentUserWithPerimeters currentUserWithPerimeters; + private String processStateInPerimeter = "\"card\":{\"process\":\"Process1\", \"state\":\"State1\"}"; + private String processStateNotInPerimeter = "\"card\":{\"process\":\"Process1\", \"state\":\"State2\"}"; + + public CardRoutingUtilitiesShould(){ + User user = new User(); + user.setLogin("testuser"); + user.setFirstName("Test"); + user.setLastName("User"); + + List groups = new ArrayList<>(); + groups.add("testgroup1"); + groups.add("testgroup2"); + user.setGroups(groups); + + List entities = new ArrayList<>(); + entities.add("testentity1"); + entities.add("testentity2"); + user.setEntities(entities); + + ComputedPerimeter perimeter = new ComputedPerimeter(); + perimeter.setProcess("Process1"); + perimeter.setState("State1"); + perimeter.setRights(RightsEnum.RECEIVE); + + currentUserWithPerimeters = new CurrentUserWithPerimeters(); + currentUserWithPerimeters.setUserData(user); + currentUserWithPerimeters.setComputedPerimeters(Arrays.asList(perimeter)); + } + + + private JSONObject createJSONObjectFromString(String jsonString) + { + try + { + return (JSONObject) (new JSONParser(JSONParser.MODE_PERMISSIVE)).parse(jsonString); + } + catch(ParseException e){ log.error("Error parsing", e); return null;} + } + + @Test + void checkIfUserMustReceiveTheCardUsingGroupsOnly() { + + JSONObject messageBodyWithGroupOfTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"]}"); //true + JSONObject messageBodyWithGroupOfTheUserButStateNotInPerimeter = createJSONObjectFromString("{" + processStateNotInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"]}"); //true + JSONObject messageBodyWithNoGroupOfTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"]}"); //false + JSONObject messageBodyWithGroupOfTheUserAndEmptyEntitiesList = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"], \"entityRecipientsIds\":[]}"); //true + JSONObject messageBodyWithNoGroupOfTheUserAndEmptyEntitiessList = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"], \"entityRecipientsIds\":[]}"); //false + + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithGroupOfTheUser, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithGroupOfTheUserButStateNotInPerimeter, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithNoGroupOfTheUser, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithGroupOfTheUserAndEmptyEntitiesList, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithNoGroupOfTheUserAndEmptyEntitiessList, currentUserWithPerimeters)).isFalse(); + + } + + @Test + void checkIfUserMustReceiveTheCardUsingEntitiesOnly() { + + + JSONObject messageBodyWithEntityOfTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ", \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); + JSONObject messageBodyWithEntityOfTheUserButStateNotInPerimeter = createJSONObjectFromString("{" + processStateNotInPerimeter + ", \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); + JSONObject messageBodyWithNoEntityOfTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ", \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); + JSONObject messageBodyWithEntityOfTheUserAndEmptyGroupsList = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[], \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); + JSONObject messageBodyWithNoEntityOfTheUserAndEmptyGroupsList = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[], \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); + + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEntityOfTheUser, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEntityOfTheUserButStateNotInPerimeter, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithNoEntityOfTheUser, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEntityOfTheUserAndEmptyGroupsList, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithNoEntityOfTheUserAndEmptyGroupsList, currentUserWithPerimeters)).isFalse(); + + } + + + @Test + void checkIfUserMustReceiveTheCardUsingGroupsAndEntities() { + + JSONObject messageBodyWithEntityAndGroupOfTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); //true + JSONObject messageBodyWithEntityAndGroupOfTheUser2 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup2\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity2\", \"testentity4\"]}"); //true + JSONObject messageBodyWithEntityAndGroupOfTheUserButStateNotInPerimeter = createJSONObjectFromString("{" + processStateNotInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); //true + JSONObject messageBodyWithGroupOfTheUserButNotEntity = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); //false (in group but not in entity) + JSONObject messageBodyWithEntityOfTheUserButNotGroup = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); //false (in entity but not in group) + JSONObject messageBodyWithNoGroupAndNoEntityOfTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); //false (not in group and not in entity) + + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEntityAndGroupOfTheUser, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEntityAndGroupOfTheUser2, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEntityAndGroupOfTheUserButStateNotInPerimeter, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithGroupOfTheUserButNotEntity, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEntityOfTheUserButNotGroup, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithNoGroupAndNoEntityOfTheUser,currentUserWithPerimeters)).isFalse(); + + } + + @Test + void checkIfUserMustReceiveTheCardUsingNoGroupsAndNoEntities() { + + JSONObject messageBodyWithEmptyRecipientAndGroup = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[], \"entityRecipientsIds\":[]}"); //false + JSONObject messageBodyWithNoRecipients = createJSONObjectFromString("{" + processStateInPerimeter + "}"); //false + + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithEmptyRecipientAndGroup, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithNoRecipients, currentUserWithPerimeters)).isFalse(); + + } + + + @Test + void checkIfUserMustReceiveTheCardUsingUserOnly() { + + JSONObject messageBodyWithTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ",\"userRecipientsIds\":[\"testuser\", \"noexistantuser2\"]}"); + JSONObject messageBodyWithTheUserAndEntity = createJSONObjectFromString("{" + processStateInPerimeter + ",\"userRecipientsIds\":[\"testuser\", \"noexistantuser2\"],\"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); + JSONObject messageBodyWithTheUserAndGroup = createJSONObjectFromString("{" + processStateInPerimeter + ",\"userRecipientsIds\":[\"testuser\", \"noexistantuser2\"], \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"]}"); + JSONObject messageBodyWithTheUserButStateNotInPerimeter = createJSONObjectFromString("{" + processStateNotInPerimeter + ",\"userRecipientsIds\":[\"testuser\", \"noexistantuser2\"]}"); + JSONObject messageBodyWithoutTheUser = createJSONObjectFromString("{" + processStateInPerimeter + ",\"userRecipientsIds\":[\"noexistantuser1\", \"noexistantuser2\"]}"); + + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithTheUser, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithTheUserAndEntity, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithTheUserAndGroup, currentUserWithPerimeters)).isTrue(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithTheUserButStateNotInPerimeter, currentUserWithPerimeters)).isFalse(); + Assertions.assertThat(CardRoutingUtilities.checkIfUserMustReceiveTheCard(messageBodyWithoutTheUser, currentUserWithPerimeters)).isFalse(); + } + + + @Test + void testCreateDeleteCardMessageForUserNotRecipient(){ + JSONObject cardAdd = createJSONObjectFromString("{\"card\":{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5b\",\"state\":\"messageState\",\"startDate\":1592396243446},\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"ADD\"}"); + JSONObject cardAddWantedOutput = createJSONObjectFromString("{\"card\":{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5b\",\"state\":\"messageState\",\"startDate\":1592396243446},\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"DELETE\",\"cardId\":\"api_test_process5b\"}"); + JSONObject cardAddOutput = createJSONObjectFromString(CardRoutingUtilities.createDeleteCardMessageForUserNotRecipient(cardAdd,"test")); + Assertions.assertThat(cardAddOutput).isEqualTo(cardAddWantedOutput); + + String messageBodyDelete = "{\"card\":{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5d\",\"state\":\"messageState\",\"startDate\":1592396243446},\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"DELETE\"}"; + JSONObject inputDelete = createJSONObjectFromString(messageBodyDelete); + String outputDelete = CardRoutingUtilities.createDeleteCardMessageForUserNotRecipient(inputDelete,"test"); + Assertions.assertThat(outputDelete).isEmpty(); + } +} diff --git a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/services/CardSubscriptionServiceShould.java b/services/cards-consultation/src/test/java/org/opfab/cards/consultation/services/CardSubscriptionServiceShould.java deleted file mode 100644 index bc24f73d32..0000000000 --- a/services/cards-consultation/src/test/java/org/opfab/cards/consultation/services/CardSubscriptionServiceShould.java +++ /dev/null @@ -1,246 +0,0 @@ -/* Copyright (c) 2018-2021, RTE (http://www.rte-france.com) - * See AUTHORS.txt - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * SPDX-License-Identifier: MPL-2.0 - * This file is part of the OperatorFabric project. - */ - -package org.opfab.cards.consultation.services; - -import lombok.extern.slf4j.Slf4j; -import org.assertj.core.api.Assertions; -import org.awaitility.core.ConditionTimeoutException; -import net.minidev.json.JSONObject; -import net.minidev.json.parser.JSONParser; -import net.minidev.json.parser.ParseException; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.opfab.cards.consultation.application.IntegrationTestApplication; -import org.opfab.springtools.configuration.test.UserServiceCacheTestApplication; -import org.opfab.users.model.ComputedPerimeter; -import org.opfab.users.model.CurrentUserWithPerimeters; -import org.opfab.users.model.RightsEnum; -import org.opfab.users.model.User; -import org.springframework.amqp.core.FanoutExchange; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import reactor.test.StepVerifier; - - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.awaitility.Awaitility.await; - -/** - *

- * Created on 29/10/18 - * - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = {IntegrationTestApplication.class,CardSubscriptionService.class, - UserServiceCacheTestApplication.class}) -@Slf4j -@ActiveProfiles("test") -@Tag("end-to-end") -@Tag("amqp") -public class CardSubscriptionServiceShould { - - private static String TEST_ID = "testClient"; - - @Autowired - private RabbitTemplate rabbitTemplate; - @Autowired - private FanoutExchange cardExchange; - @Autowired - private CardSubscriptionService service; - @Autowired - private ThreadPoolTaskScheduler taskScheduler; - private CurrentUserWithPerimeters currentUserWithPerimeters; - - private static String rabbitTestMessage = "{\"card\":{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"Process1\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5b\",\"state\":\"State1\",\"startDate\":1592396243446},\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"testgroup1\"],\"type\":\"ADD\"}"; - - public CardSubscriptionServiceShould(){ - User user = new User(); - user.setLogin("testuser"); - user.setFirstName("Test"); - user.setLastName("User"); - - List groups = new ArrayList<>(); - groups.add("testgroup1"); - groups.add("testgroup2"); - user.setGroups(groups); - - List entities = new ArrayList<>(); - entities.add("testentity1"); - entities.add("testentity2"); - user.setEntities(entities); - - ComputedPerimeter perimeter = new ComputedPerimeter(); - perimeter.setProcess("Process1"); - perimeter.setState("State1"); - perimeter.setRights(RightsEnum.RECEIVE); - - currentUserWithPerimeters = new CurrentUserWithPerimeters(); - currentUserWithPerimeters.setUserData(user); - currentUserWithPerimeters.setComputedPerimeters(Arrays.asList(perimeter)); - } - - @Test - void createAndDeleteSubscription(){ - CardSubscription subscription = service.subscribe(currentUserWithPerimeters, TEST_ID); - subscription.getPublisher().subscribe(log::info); - Assertions.assertThat(subscription.checkActive()).isTrue(); - service.evict(subscription.getId()); - Assertions.assertThat(subscription.isCleared()).isTrue(); - Assertions.assertThat(subscription.checkActive()).isFalse(); -// await().atMost(10, TimeUnit.SECONDS).until(() -> !subscription.checkActive()); - } - - @Test - void deleteSubscriptionWithDelay(){ - CardSubscription subscription = service.subscribe(currentUserWithPerimeters, TEST_ID); - subscription.getPublisher().subscribe(log::info); - Assertions.assertThat(subscription.checkActive()).isTrue(); - service.scheduleEviction(subscription.getId()); - Assertions.assertThat(subscription.checkActive()).isTrue(); - Assertions.assertThat(subscription.isCleared()).isFalse(); - await().atMost(15, TimeUnit.SECONDS).until(() -> !subscription.checkActive() && subscription.isCleared()); - } - - @Test - void reviveSubscription(){ - CardSubscription subscription = service.subscribe(currentUserWithPerimeters, TEST_ID); - subscription.getPublisher().subscribe(log::info); - Assertions.assertThat(subscription.checkActive()).isTrue(); - service.scheduleEviction(subscription.getId()); - Assertions.assertThat(subscription.checkActive()).isTrue(); - try { - await().atMost(6, TimeUnit.SECONDS).until(() -> !subscription.checkActive() && subscription.isCleared()); - Assertions.assertThat(false).describedAs("An exception was expected here").isFalse(); - }catch (ConditionTimeoutException e){ - //nothing, everything is alright - } - CardSubscription subscription2 = service.subscribe(currentUserWithPerimeters, TEST_ID); - Assertions.assertThat(subscription2).isSameAs(subscription); - try { - await().atMost(6, TimeUnit.SECONDS).until(() -> !subscription.checkActive() && subscription.isCleared()); - Assertions.assertThat(false).describedAs("An exception was expected here").isFalse(); - }catch (ConditionTimeoutException e){ - //nothing, everything is alright - } - service.evict(subscription.getId()); - Assertions.assertThat(subscription.isCleared()).isTrue(); - Assertions.assertThat(subscription.checkActive()).isFalse(); - } - - @Test - void receiveCards(){ - CardSubscription subscription = service.subscribe(currentUserWithPerimeters, TEST_ID); - StepVerifier.FirstStep verifier = StepVerifier.create(subscription.getPublisher().filter(m -> !m.equals("HEARTBEAT") && !m.equals("BUSINESS_CONFIG_CHANGE") && !m.equals("USER_CONFIG_CHANGE") )); - taskScheduler.schedule(createSendMessageTask(),new Date(System.currentTimeMillis() + 1000)); - verifier - .expectNext("INIT") - .expectNext(rabbitTestMessage) - .expectNext(rabbitTestMessage) - .thenCancel() - .verify(); - } - - private Runnable createSendMessageTask() { - return () ->{ - - rabbitTemplate.convertAndSend(cardExchange.getName(), currentUserWithPerimeters.getUserData().getLogin(),rabbitTestMessage); - rabbitTemplate.convertAndSend(cardExchange.getName(), currentUserWithPerimeters.getUserData().getLogin(),rabbitTestMessage); - }; - } - - private JSONObject createJSONObjectFromString(String jsonString) - { - try - { - return (JSONObject) (new JSONParser(JSONParser.MODE_PERMISSIVE)).parse(jsonString); - } - catch(ParseException e){ log.error("Error parsing", e); return null;} - } - - @Test - void testCheckIfUserMustReceiveTheCard() { - CardSubscription subscription = service.subscribe(currentUserWithPerimeters, TEST_ID); - - //groups only - String processStateInPerimeter = "\"card\":{\"process\":\"Process1\", \"state\":\"State1\"}"; - JSONObject messageBody1 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"]}"); //true - JSONObject messageBody2 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"]}"); //false - JSONObject messageBody3 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"], \"entityRecipientsIds\":[]}"); //true - JSONObject messageBody4 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"], \"entityRecipientsIds\":[]}"); //false - - //entities only - JSONObject messageBody5 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); //true - JSONObject messageBody6 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); //false - JSONObject messageBody7 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[], \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); //true - JSONObject messageBody8 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[], \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); //false - - //groups and entities - JSONObject messageBody9 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); //true - JSONObject messageBody10 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup2\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity2\", \"testentity4\"]}"); //true - JSONObject messageBody11 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup1\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); //false (in group but not in entity) - JSONObject messageBody12 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity1\", \"testentity4\"]}"); //false (in entity but not in group) - JSONObject messageBody13 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[\"testgroup3\", \"testgroup4\"], \"entityRecipientsIds\":[\"testentity3\", \"testentity4\"]}"); //false (not in group and not in entity) - - //no groups and no entities - JSONObject messageBody14 = createJSONObjectFromString("{" + processStateInPerimeter + ", \"groupRecipientsIds\":[], \"entityRecipientsIds\":[]}"); //false - JSONObject messageBody15 = createJSONObjectFromString("{" + processStateInPerimeter + "}"); //false - - // users only - JSONObject messageBody16 = createJSONObjectFromString("{" + processStateInPerimeter + ",\"userRecipientsIds\":[\"testuser\", \"noexistantuser2\"]}"); //true - JSONObject messageBody17 = createJSONObjectFromString("{" + processStateInPerimeter + ",\"userRecipientsIds\":[\"noexistantuser1\", \"noexistantuser2\"]}"); //false - - - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody1, currentUserWithPerimeters)).isTrue(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody2, currentUserWithPerimeters)).isFalse(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody3, currentUserWithPerimeters)).isTrue(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody4, currentUserWithPerimeters)).isFalse(); - - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody5, currentUserWithPerimeters)).isTrue(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody6, currentUserWithPerimeters)).isFalse(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody7, currentUserWithPerimeters)).isTrue(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody8, currentUserWithPerimeters)).isFalse(); - - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody9, currentUserWithPerimeters)).isTrue(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody10, currentUserWithPerimeters)).isTrue(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody11, currentUserWithPerimeters)).isFalse(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody12, currentUserWithPerimeters)).isFalse(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody13,currentUserWithPerimeters)).isFalse(); - - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody14, currentUserWithPerimeters)).isFalse(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody15, currentUserWithPerimeters)).isFalse(); - - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody16, currentUserWithPerimeters)).isTrue(); - Assertions.assertThat(subscription.checkIfUserMustReceiveTheCard(messageBody17, currentUserWithPerimeters)).isFalse(); - } - - - @Test - void testCreateDeleteCardMessageForUserNotRecipient(){ - CardSubscription subscription = service.subscribe(currentUserWithPerimeters, TEST_ID); - - String messageBodyAdd = "{\"cards\":[{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5b\",\"state\":\"messageState\",\"startDate\":1592396243446}],\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"ADD\"}"; - String messageBodyUpdate = "{\"cards\":[{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5c\",\"state\":\"messageState\",\"startDate\":1592396243446}],\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"UPDATE\"}"; - String messageBodyDelete = "{\"cards\":[{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5d\",\"state\":\"messageState\",\"startDate\":1592396243446}],\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"DELETE\"}"; - - Assertions.assertThat(subscription.createDeleteCardMessageForUserNotRecipient(createJSONObjectFromString(messageBodyAdd)).equals("{\"cards\":[{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5b\",\"state\":\"messageState\",\"startDate\":1592396243446}],\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"DELETE\",\"cardIds\":\"api_test_process5b\"}")); - Assertions.assertThat(subscription.createDeleteCardMessageForUserNotRecipient(createJSONObjectFromString(messageBodyUpdate)).equals("{\"cards\":[{\"severity\":\"ALARM\",\"summary\":{\"parameters\":{},\"key\":\"defaultProcess.summary\"},\"process\":\"defaultProcess\",\"publishDate\":1592389043000,\"title\":{\"parameters\":{},\"key\":\"defaultProcess.title\"},\"uid\":\"db914230-a5aa-42f2-aa29-f5348700fa55\",\"publisherVersion\":\"1\",\"processInstanceId\":\"process5b\",\"publisher\":\"api_test\",\"id\":\"api_test_process5b\",\"state\":\"messageState\",\"startDate\":1592396243446}],\"publishDate\":1592389043000,\"groupRecipientsIds\":[\"Dispatcher\"],\"type\":\"DELETE\",\"cardIds\":\"api_test_process5c\"}")); - Assertions.assertThat(subscription.createDeleteCardMessageForUserNotRecipient(createJSONObjectFromString(messageBodyDelete)).equals(messageBodyDelete)); //message must not be changed - } -} diff --git a/services/cards-publication/src/main/java/org/opfab/cards/publication/configuration/oauth2/WebSecurityConfiguration.java b/services/cards-publication/src/main/java/org/opfab/cards/publication/configuration/oauth2/WebSecurityConfiguration.java index c61a6b6935..a3d7e614b1 100644 --- a/services/cards-publication/src/main/java/org/opfab/cards/publication/configuration/oauth2/WebSecurityConfiguration.java +++ b/services/cards-publication/src/main/java/org/opfab/cards/publication/configuration/oauth2/WebSecurityConfiguration.java @@ -37,7 +37,10 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { public static final String PROMETHEUS_PATH ="/actuator/prometheus**"; - + public static final String LOGGERS_PATH ="/actuator/loggers/**"; + + public static final String ADMIN_ROLE = "ADMIN"; + public static final String AUTH_AND_IP_ALLOWED = "isAuthenticated() and @webSecurityChecks.checkUserIpAddress(authentication)"; public static final String ADMIN_AND_IP_ALLOWED = "hasRole('ADMIN') and @webSecurityChecks.checkUserIpAddress(authentication)"; @@ -65,6 +68,7 @@ public static void configureCommon(final HttpSecurity http, boolean checkAuthent http .authorizeRequests() .antMatchers(HttpMethod.GET,PROMETHEUS_PATH).permitAll() + .antMatchers(LOGGERS_PATH).hasRole(ADMIN_ROLE) .antMatchers("/cards/userCard/**").access(AUTH_AND_IP_ALLOWED) .antMatchers("/cards/translateCardField").access(AUTH_AND_IP_ALLOWED) .antMatchers(HttpMethod.DELETE, "/cards").access(ADMIN_AND_IP_ALLOWED) @@ -72,6 +76,7 @@ public static void configureCommon(final HttpSecurity http, boolean checkAuthent } else { http .authorizeRequests() + .antMatchers(LOGGERS_PATH).hasRole(ADMIN_ROLE) .antMatchers("/cards/userCard/**").access(AUTH_AND_IP_ALLOWED) .antMatchers("/cards/translateCardField").access(AUTH_AND_IP_ALLOWED) .antMatchers("/**").permitAll(); diff --git a/services/cards-publication/src/main/java/org/opfab/cards/publication/model/CardPublicationData.java b/services/cards-publication/src/main/java/org/opfab/cards/publication/model/CardPublicationData.java index 68b3551bea..d78c351a7d 100644 --- a/services/cards-publication/src/main/java/org/opfab/cards/publication/model/CardPublicationData.java +++ b/services/cards-publication/src/main/java/org/opfab/cards/publication/model/CardPublicationData.java @@ -74,7 +74,9 @@ public class CardPublicationData implements Card { private String summaryTranslated; @CreatedDate + @Indexed private Instant publishDate; + private Instant lttd; @Indexed diff --git a/services/cards-publication/src/main/modeling/swagger.yaml b/services/cards-publication/src/main/modeling/swagger.yaml index 73d930a7bf..a8ca0e436c 100755 --- a/services/cards-publication/src/main/modeling/swagger.yaml +++ b/services/cards-publication/src/main/modeling/swagger.yaml @@ -1,7 +1,7 @@ swagger: '2.0' info: description: IMPORTANT - The Try it Out button will generate curl requests for examples, but executing them through the UI will not work as authentication has not been set up. This page is for documentation only. - version: 3.2.0.RELEASE + version: 3.3.0.RELEASE title: Card Management API termsOfService: '' contact: diff --git a/services/external-devices/build.gradle b/services/external-devices/build.gradle index 2cf4221346..604a5d48e0 100755 --- a/services/external-devices/build.gradle +++ b/services/external-devices/build.gradle @@ -76,11 +76,9 @@ tasks.named("processResources") { } docker { - if (project.version.toUpperCase().equals("SNAPSHOT")) - name "lfeoperatorfabric/of-${project.name.toLowerCase()}:SNAPSHOT" /* more information : https://vsupalov.com/docker-latest-tag/ */ - else - name "lfeoperatorfabric/of-${project.name.toLowerCase()}" - tags "latest", "${project.version}" + name "lfeoperatorfabric/of-${project.name}:${project.version}" + if (!project.version.equals("SNAPSHOT")) + tag "latest", "latest" labels (['project':"${project.group}"]) files( "build/libs" , "../../config/docker/common-docker.yml" diff --git a/services/external-devices/src/main/java/org/opfab/externaldevices/configuration/oauth2/WebSecurityConfiguration.java b/services/external-devices/src/main/java/org/opfab/externaldevices/configuration/oauth2/WebSecurityConfiguration.java index 06225b17ef..c325ca83dd 100644 --- a/services/external-devices/src/main/java/org/opfab/externaldevices/configuration/oauth2/WebSecurityConfiguration.java +++ b/services/external-devices/src/main/java/org/opfab/externaldevices/configuration/oauth2/WebSecurityConfiguration.java @@ -30,12 +30,14 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { public static final String PROMETHEUS_PATH ="/actuator/prometheus**"; - - public static final String CONFIGURATIONS_ROOT_PATH = "/configurations"; - public static final String DEVICE_CONFIGURATIONS_PATH = CONFIGURATIONS_ROOT_PATH+"/devices/**"; - public static final String DEVICES_ROOT_PATH = "/devices"; + public static final String LOGGERS_PATH ="/actuator/loggers/**"; + public static final String CONFIGURATIONS_ROOT_PATH = "/configurations/"; + public static final String CONFIGURATIONS_USERS_PATH = CONFIGURATIONS_ROOT_PATH + "users/{login}"; + public static final String DEVICES_ROOT_PATH = "/devices/"; public static final String NOTIFICATIONS_ROOT_PATH = "/notifications"; + public static final String ADMIN_ROLE = "ADMIN"; + public static final String AUTH_AND_IP_ALLOWED = "isAuthenticated() and @webSecurityChecks.checkUserIpAddress(authentication)"; public static final String ADMIN_AND_IP_ALLOWED = "hasRole('ADMIN') and @webSecurityChecks.checkUserIpAddress(authentication)"; @@ -58,12 +60,11 @@ public static void configureCommon(final HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(HttpMethod.GET,PROMETHEUS_PATH).permitAll() + .antMatchers(LOGGERS_PATH).hasRole(ADMIN_ROLE) .antMatchers(HttpMethod.POST,NOTIFICATIONS_ROOT_PATH).access(AUTH_AND_IP_ALLOWED) - .antMatchers(HttpMethod.POST, DEVICE_CONFIGURATIONS_PATH).access(ADMIN_AND_IP_ALLOWED) - .antMatchers(HttpMethod.PATCH, DEVICE_CONFIGURATIONS_PATH).access(ADMIN_AND_IP_ALLOWED) - .antMatchers(HttpMethod.DELETE, DEVICE_CONFIGURATIONS_PATH).access(ADMIN_AND_IP_ALLOWED) - .antMatchers(HttpMethod.GET, DEVICES_ROOT_PATH).access(ADMIN_AND_IP_ALLOWED) - .antMatchers(HttpMethod.POST, DEVICES_ROOT_PATH).access(ADMIN_AND_IP_ALLOWED) + .antMatchers(HttpMethod.GET, CONFIGURATIONS_USERS_PATH).access(AUTH_AND_IP_ALLOWED) + .antMatchers(CONFIGURATIONS_ROOT_PATH+"**").access(ADMIN_AND_IP_ALLOWED) + .antMatchers(DEVICES_ROOT_PATH+"**").access(ADMIN_AND_IP_ALLOWED) .anyRequest().access(AUTH_AND_IP_ALLOWED); } diff --git a/services/external-devices/src/main/java/org/opfab/externaldevices/services/ConfigService.java b/services/external-devices/src/main/java/org/opfab/externaldevices/services/ConfigService.java index db929758f2..e0187f31a0 100644 --- a/services/external-devices/src/main/java/org/opfab/externaldevices/services/ConfigService.java +++ b/services/external-devices/src/main/java/org/opfab/externaldevices/services/ConfigService.java @@ -35,7 +35,10 @@ public class ConfigService { public static final String DEBUG_RETRIEVED_CONFIG = "Retrieved configuration for"; public static final String UNSUPPORTED_SIGNAL ="Signal %1$s is not supported in mapping %2$s"; public static final String NULL_AFTER_DELETE = "Following deletion of {}, no {} is configured for {} {}"; - public static final String DEVICE_NAME = "device"; + public static final String CANNOT_RETRIEVE_FOR_NULL_OR_EMPTY_ID = "Cannot retrieve %1$s with null or empty id."; + public static final String DEVICE_CONFIG = "device configuration"; + public static final String SIGNAL_CONFIG = "signal mapping"; + public static final String USER_CONFIG = "user configuration"; private final UserConfigurationRepository userConfigurationRepository; private final DeviceConfigurationRepository deviceConfigurationRepository; @@ -82,39 +85,45 @@ public ResolvedConfiguration getResolvedConfiguration(String opFabSignalKey, Str } public DeviceConfiguration retrieveDeviceConfiguration(String deviceId) throws ExternalDeviceConfigurationException { - - Optional deviceConfiguration = deviceConfigurationRepository.findById(deviceId); - if(deviceConfiguration.isPresent()) { - DeviceConfiguration retrievedDeviceConfig = deviceConfiguration.get(); - log.debug("{} for device {} : {}", DEBUG_RETRIEVED_CONFIG, deviceId, retrievedDeviceConfig.toString()); - return retrievedDeviceConfig; + if(deviceId == null || deviceId.isEmpty()) { + throw new ExternalDeviceConfigurationException(String.format(CANNOT_RETRIEVE_FOR_NULL_OR_EMPTY_ID, DEVICE_CONFIG, deviceId)); } else { - throw new ExternalDeviceConfigurationException(String.format(CONFIGURATION_NOT_FOUND, DEVICE_NAME, deviceId)); + Optional deviceConfiguration = deviceConfigurationRepository.findById(deviceId); + if(deviceConfiguration.isPresent()) { + DeviceConfiguration retrievedDeviceConfig = deviceConfiguration.get(); + log.debug("{} for device {} : {}", DEBUG_RETRIEVED_CONFIG, deviceId, retrievedDeviceConfig.toString()); + return retrievedDeviceConfig; + } else { + throw new ExternalDeviceConfigurationException(String.format(CONFIGURATION_NOT_FOUND, DEVICE_CONFIG, deviceId)); + } } - } public UserConfiguration retrieveUserConfiguration(String userLogin) throws ExternalDeviceConfigurationException { - - Optional userConfiguration = userConfigurationRepository.findById(userLogin); - if(userConfiguration.isPresent()) { - UserConfiguration retrievedUserConfig = userConfiguration.get(); - log.debug("{} for user {} : {}", DEBUG_RETRIEVED_CONFIG, userLogin, retrievedUserConfig.toString()); - return retrievedUserConfig; + if(userLogin == null || userLogin.isEmpty()) { + throw new ExternalDeviceConfigurationException(String.format(CANNOT_RETRIEVE_FOR_NULL_OR_EMPTY_ID, USER_CONFIG)); } else { - throw new ExternalDeviceConfigurationException(String.format(CONFIGURATION_NOT_FOUND, "user", userLogin)); + Optional userConfiguration = userConfigurationRepository.findById(userLogin); + if(userConfiguration.isPresent()) { + UserConfiguration retrievedUserConfig = userConfiguration.get(); + log.debug("{} for user {} : {}", DEBUG_RETRIEVED_CONFIG, userLogin, retrievedUserConfig.toString()); + return retrievedUserConfig; + } else { + throw new ExternalDeviceConfigurationException(String.format(CONFIGURATION_NOT_FOUND, USER_CONFIG, userLogin)); + } } - } public SignalMapping retrieveSignalMapping(String signalMappingId) throws ExternalDeviceConfigurationException { - + if(signalMappingId == null || signalMappingId.isEmpty()) { + throw new ExternalDeviceConfigurationException(String.format(CANNOT_RETRIEVE_FOR_NULL_OR_EMPTY_ID, SIGNAL_CONFIG)); + } Optional signalMapping = signalMappingRepository.findById(signalMappingId); if(signalMapping.isPresent()) { SignalMapping retrievedSignalMapping = signalMapping.get(); log.debug("{} for signal {} : {}", DEBUG_RETRIEVED_CONFIG, signalMappingId, retrievedSignalMapping.toString()); return retrievedSignalMapping; } else { - throw new ExternalDeviceConfigurationException(String.format(CONFIGURATION_NOT_FOUND, "signal", signalMappingId)); + throw new ExternalDeviceConfigurationException(String.format(CONFIGURATION_NOT_FOUND, SIGNAL_CONFIG, signalMappingId)); } } @@ -129,7 +138,7 @@ public void deleteDeviceConfiguration(String deviceId) throws ExternalDeviceConf if (foundUserConfigurations != null) { for (UserConfigurationData userConfigurationData : foundUserConfigurations) { userConfigurationData.setExternalDeviceId(null); - log.warn(NULL_AFTER_DELETE, deviceId, DEVICE_NAME, "user", userConfigurationData.getUserLogin()); + log.warn(NULL_AFTER_DELETE, deviceId, DEVICE_CONFIG, "user", userConfigurationData.getUserLogin()); } userConfigurationRepository.saveAll(foundUserConfigurations); } @@ -150,7 +159,7 @@ public void deleteSignalMapping(String signalMappingId) throws ExternalDeviceCon if (foundDeviceConfigurations != null) { for (DeviceConfigurationData deviceConfigurationData : foundDeviceConfigurations) { deviceConfigurationData.setSignalMappingId(null); - log.warn(NULL_AFTER_DELETE, signalMappingId, "signalMapping", DEVICE_NAME, deviceConfigurationData.getId()); + log.warn(NULL_AFTER_DELETE, signalMappingId, "signalMapping", DEVICE_CONFIG, deviceConfigurationData.getId()); } deviceConfigurationRepository.saveAll(foundDeviceConfigurations); } diff --git a/services/external-devices/src/main/modeling/swagger.yaml b/services/external-devices/src/main/modeling/swagger.yaml index cb513e4426..29debf2467 100755 --- a/services/external-devices/src/main/modeling/swagger.yaml +++ b/services/external-devices/src/main/modeling/swagger.yaml @@ -1,7 +1,7 @@ swagger: '2.0' info: description: IMPORTANT - The Try it Out button will generate curl requests for examples, but executing them through the UI will not work as authentication has not been set up. This page is for documentation only. - version: 3.2.0.RELEASE + version: 3.3.0.RELEASE title: External Devices Management termsOfService: '' contact: diff --git a/services/external-devices/src/test/java/org/opfab/externaldevices/services/ConfigServiceShould.java b/services/external-devices/src/test/java/org/opfab/externaldevices/services/ConfigServiceShould.java index 09a4c798b8..1da693a460 100644 --- a/services/external-devices/src/test/java/org/opfab/externaldevices/services/ConfigServiceShould.java +++ b/services/external-devices/src/test/java/org/opfab/externaldevices/services/ConfigServiceShould.java @@ -125,14 +125,6 @@ void retrieveExistingDeviceConfiguration() throws ExternalDeviceConfigurationExc } - @Test - void throwExceptionIfDeviceConfigurationToRetrieveDoesNotExist() { - - assertThrows(ExternalDeviceConfigurationException.class, - () -> configService.retrieveDeviceConfiguration("device_configuration_that_doesnt_exist")); - - } - @Test void deleteExistingDeviceConfiguration() throws ExternalDeviceConfigurationException { @@ -217,14 +209,6 @@ void retrieveExistingUserConfiguration() throws ExternalDeviceConfigurationExcep } - @Test - void throwExceptionIfUserConfigurationToRetrieveDoesNotExist() { - - assertThrows(ExternalDeviceConfigurationException.class, - () -> configService.retrieveUserConfiguration("user_configuration_that_doesnt_exist")); - - } - @Test void deleteExistingSignalMapping() throws ExternalDeviceConfigurationException { @@ -331,6 +315,27 @@ void throwErrorIfConfigurationCantBeResolved(String userLogin, String opFabSigna () -> configService.getResolvedConfiguration(opFabSignalKey, userLogin)); } + @ParameterizedTest + @MethodSource("retrieveConfigurationErrorParams") + void throwExceptionWhenAttemptingToRetrieveUserConfiguration(String userLogin) { + assertThrows(ExternalDeviceConfigurationException.class, + () -> configService.retrieveUserConfiguration(userLogin)); + } + + @ParameterizedTest + @MethodSource("retrieveConfigurationErrorParams") + void throwExceptionWhenAttemptingToRetrieveDeviceConfiguration(String deviceId) { + assertThrows(ExternalDeviceConfigurationException.class, + () -> configService.retrieveDeviceConfiguration(deviceId)); + } + + @ParameterizedTest + @MethodSource("retrieveConfigurationErrorParams") + void throwExceptionWhenAttemptingToRetrieveSignalMapping(String signalMappingId) { + assertThrows(ExternalDeviceConfigurationException.class, + () -> configService.retrieveSignalMapping(signalMappingId)); + } + @AfterEach public void clean(){ signalMappingRepository.deleteAll(); @@ -434,6 +439,14 @@ private static Stream getResolvedConfigurationErrorParams() { ); } + private static Stream retrieveConfigurationErrorParams() { + return Stream.of( + Arguments.of("item_that_doesnt_exist"), + Arguments.of(""), + null + ); + } + } diff --git a/services/services.gradle b/services/services.gradle index 567b3b7e7c..4d8a3cdba0 100755 --- a/services/services.gradle +++ b/services/services.gradle @@ -111,11 +111,9 @@ subprojects { } docker { - if (project.version.toUpperCase().equals("SNAPSHOT")) - name "lfeoperatorfabric/of-${project.name.toLowerCase()}:${project.version.toUpperCase()}" /* more information : https://vsupalov.com/docker-latest-tag/ */ - else - name "lfeoperatorfabric/of-${project.name.toLowerCase()}" - tags "latest", "${project.version}" + name "lfeoperatorfabric/of-${project.name}:${project.version}" + if (!project.version.equals("SNAPSHOT")) + tag "latest", "latest" labels (['project':"${project.group}"]) files( "build/libs" , "../../config/docker/common-docker.yml" diff --git a/services/users/src/main/java/org/opfab/users/configuration/oauth2/WebSecurityConfiguration.java b/services/users/src/main/java/org/opfab/users/configuration/oauth2/WebSecurityConfiguration.java index a1447bc7bf..4d6feff10f 100644 --- a/services/users/src/main/java/org/opfab/users/configuration/oauth2/WebSecurityConfiguration.java +++ b/services/users/src/main/java/org/opfab/users/configuration/oauth2/WebSecurityConfiguration.java @@ -33,6 +33,8 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { public static final String PROMETHEUS_PATH ="/actuator/prometheus**"; + public static final String LOGGERS_PATH ="/actuator/loggers/**"; + public static final String USER_PATH = "/users/{login}"; public static final String USERS_SETTINGS_PATH = "/users/{login}/settings"; public static final String USERS_PERIMETERS_PATH = "/users/{login}/perimeters"; @@ -87,6 +89,7 @@ public static void configureCommon(final HttpSecurity http) throws Exception { .antMatchers(ENTITIES_PATH).access(IS_ADMIN_AND_IP_ALLOWED) .antMatchers(PERIMETERS_PATH).access(IS_ADMIN_AND_IP_ALLOWED) .antMatchers(CURRENTUSER_INTERNAL_PATH).authenticated() + .antMatchers(LOGGERS_PATH).hasRole(ADMIN_ROLE) .anyRequest().access(AUTH_AND_IP_ALLOWED); } diff --git a/services/users/src/main/modeling/swagger.yaml b/services/users/src/main/modeling/swagger.yaml index e8c48389c3..79ba0c387f 100755 --- a/services/users/src/main/modeling/swagger.yaml +++ b/services/users/src/main/modeling/swagger.yaml @@ -1,7 +1,7 @@ swagger: '2.0' info: description: IMPORTANT - The Try it Out button will generate curl requests for examples, but executing them through the UI will not work as authentication has not been set up. This page is for documentation only. - version: 3.2.0.RELEASE + version: 3.3.0.RELEASE title: User Management termsOfService: '' contact: diff --git a/src/docs/asciidoc/deployment/index.adoc b/src/docs/asciidoc/deployment/index.adoc index f4423fc90b..c17269a3dd 100644 --- a/src/docs/asciidoc/deployment/index.adoc +++ b/src/docs/asciidoc/deployment/index.adoc @@ -36,6 +36,48 @@ include::RABBITMQ.adoc[leveloffset=+1] Operator Fabric provides end points for monitoring via link:https://prometheus.io/[prometheus]. The monitoring is available for the four following services: user, businessconfig, cards-consultation, cards-publication. You can start a test prometheus instance via `config/monitoring/startPrometheus.sh` , the monitoring will be accessible on http://localhost:9090/ +== Logging Administration + +Operator Fabric includes the ability to view and configure the log levels at runtime through APIs. It is possible to configure and view an individual logger configuration, which is made up of both the explicitly configured logging level as well as the effective logging level given to it by the logging framework. These levels can be one of: + +* TRACE +* DEBUG +* INFO +* WARN +* ERROR +* FATAL +* OFF +* null + +null indicates that there is no explicit configuration. + +Querying and setting logging levels is restricted to administrators. + +To view the configured logging level for a given logger it is possible to send a GET request to the '/actuator/logger' URI as follows: +---- +curl http://:/actuator/loggers/${logger} -H "Authorization: Bearer ${token}" -H "Content-type:application/json" +---- +where `${token}` is a valid OAuth2 JWT for a user with administration privileges +and `${logger}` is the logger (ex: org.opfab) + +The response will be a json object like the following: + +---- +{ + "configuredLevel" : "INFO", + "effectiveLevel" : "INFO" +} +---- + +To configure a given logger, POST a json entity to the '/actuator/logger' URI, as follows: + +---- +curl -i -X POST http://:/actuator/loggers/${logger} -H "Authorization: Bearer ${token}" -H 'Content-Type: application/json' -d '{"configuredLevel": "DEBUG"}' +---- + +To “reset” the specific level of the logger (and use the default configuration instead) it is possible to pass a value of null as the configuredLevel. + + include::users_groups_admin.adoc[leveloffset=+1] diff --git a/src/docs/asciidoc/dev_env/project_structure.adoc b/src/docs/asciidoc/dev_env/project_structure.adoc index 8baba16a78..689f44e0fe 100644 --- a/src/docs/asciidoc/dev_env/project_structure.adoc +++ b/src/docs/asciidoc/dev_env/project_structure.adoc @@ -43,6 +43,7 @@ tests and demonstrations ** link:https://github.com/opfab/operatorfabric-core/tree/master/src/test[test] *** link:https://github.com/opfab/operatorfabric-core/tree/master/src/test/api[api] : karate code for automatic api testing (non-regression tests) *** link:https://github.com/opfab/operatorfabric-core/tree/master/src/test/cypress[cypress] : cypress code for automatic ui testing +*** link:https://github.com/opfab/operatorfabric-core/tree/master/src/test/dummyModbusDevice[dummyModbusDevice] : application emulating a Modbus device for test purposes *** link:https://github.com/opfab/operatorfabric-core/tree/master/src/test/resources[resources] : scripts and data for manual testing * link:https://github.com/opfab/operatorfabric-core/tree/master/tools[tools] ** link:https://github.com/opfab/operatorfabric-core/tree/master/tools/generic[generic]: Generic (as opposed to Spring-related) diff --git a/src/docs/asciidoc/dev_env/testing_ext_dev.adoc b/src/docs/asciidoc/dev_env/testing_ext_dev.adoc new file mode 100644 index 0000000000..9ba47cf6b7 --- /dev/null +++ b/src/docs/asciidoc/dev_env/testing_ext_dev.adoc @@ -0,0 +1,58 @@ +// Copyright (c) 2021 RTE (http://www.rte-france.com) +// See AUTHORS.txt +// This document is subject to the terms of the Creative Commons Attribution 4.0 International license. +// If a copy of the license was not distributed with this +// file, You can obtain one at https://creativecommons.org/licenses/by/4.0/. +// SPDX-License-Identifier: CC-BY-4.0 + += Testing the External Devices service with dummy devices + +== Docker mode + +To test the External Devices service with dummy devices, launch an OperatorFabric instance using the +`$OF_HOME/config/docker/docker-compose.sh` script. + +As described in `$OF_HOME/config/docker/docker-compose.yml`, in addition to lauching all OperatorFabric services it +will also start up two containers based on the dummy-modbus-device image: `dummy-modbus-device_1` and +`dummy-modbus-device_2`, both listening on their port 4030. + +This matches default configuration provided: +.$OF_HOME/config/docker/external-devices-docker.yml + +[source,yaml] +---- +include::../../../../config/docker/external-devices-docker.yml[] +---- + +This means that provided the users have chosen to play sounds for these severities and to play sounds +on external devices, sending an ALARM card to operator1 should result in a message about writing 1 in the +dummy-modbus-device_1 logs, and sending an ACTION card to operator2 should result in a message about writing +6 in dummy-modbus-device_2. + +Since the watchdog is enabled, once the devices are connected (either by calling the /connect endpoint or +by sending the first signal), you should also see the corresponding messages in the logs. + +== Dev mode + +To test in "dev" mode + +---- +$OF_HOME/config/dev/docker-compose.sh <1> +$OF_HOME/bin/run_all.sh status --externalDevices <2> +$OF_HOME/src/test/dummyModbusDevice/src/main/bin/startDummy.sh <3> +---- +<1> Launch supporting containers (MongoDB, Keycloak, etc.) +<2> Launch OperatorFabric services (including External Devices) +<3> Start a dummy modbus device + +NOTE: Configuration for the dummy modbus device can be provided in +`src/test/dummyModbusDevice/src/main/resources/application.yml`. + +NOTE: As only one dummy Modbus device is started in this case, the default configuration for external devices has been +adapted from the one provided in docker mode so both `CDS_1` and `CDS_2` point to the same device, but with different +signal mappings. The mapping for `CDS_2` is "broken" on purpose, as it attempts to write in registers that are outside +the allowed register range, to demonstrate exceptions. + +== Dummy devices configuration + +See the https://github.com/opfab/operatorfabric-core/blob/develop/src/test/dummyModbusDevice/README.adoc[README for the Dummy Modbus Device module]. diff --git a/src/docs/asciidoc/images/ExtDevArchitecture.drawio.png b/src/docs/asciidoc/images/ExtDevArchitecture.drawio.png new file mode 100644 index 0000000000..eeead04db5 Binary files /dev/null and b/src/docs/asciidoc/images/ExtDevArchitecture.drawio.png differ diff --git a/src/docs/asciidoc/reference_doc/external_devices_service.adoc b/src/docs/asciidoc/reference_doc/external_devices_service.adoc index be9b2ea3bd..a6bf0d6833 100644 --- a/src/docs/asciidoc/reference_doc/external_devices_service.adoc +++ b/src/docs/asciidoc/reference_doc/external_devices_service.adoc @@ -43,37 +43,142 @@ protocols (and ultimately allow people to supply their own drivers) in the futur == Implementation -Given the use case described above, the following elements need to be configurable: - -* How to connect to a given external device (host, port) -* How to map an OperatorFabric signal key footnote:[currently, that means a severity, but in the future it could also +=== Architecture + +Given the use case described above, a new service was necessary to act as a link between the OperatorFabric UI and +the external devices. This service is in charge of: + +* Managing the configuration relative to external devices (see below for details) +* Process requests from the UI (e.g. "play the sound for ALARM for user operator1") and translate them as requests to +the appropriate device in their supported protocol, based on the above configuration +* Allow the pool of devices to be managed (connection, disconnection) + +This translates as three APIs. + +image::ExtDevArchitecture.drawio.png[Architecture diagram] + +Here is what happens when user operator1 receives a card with severity ALARM: + +. In the Angular code, the reception of the card triggers a sound notification. +. If the external devices feature is enabled and the user has chosen to play sounds on external devices +(instead of the browser), the UI code sends a POST request on the `external-devices/notifications` endpoint on the +NGINX gateway, with the following payload: ++ +[source,json] +---- +{ + "opfabSignalId": "ALARM" +} +---- ++ +. The NGINX server, acting as a gateway, forwards it to the `/notifications` endpoint on the External Devices service. +. The External Devices service queries the configuration repositories to find out which external device is configured +for operator1, how to connect to it and what signal "ALARM" translates to on this particular device. +. It then creates the appropriate connection if it doesn't exist yet, and sends the signal. + +=== Configuration + +The following elements need to be configurable: + +. For each user, which device to use: ++ +.userConfiguration +[source,json,] +---- +{ + "userLogin": "operator1", + "externalDeviceId": "CDS_1" +} +---- ++ +. How to connect to a given external device (host, port) ++ +.deviceConfiguration +[source,json,] +---- +{ + "id": "CDS_1", + "host": "localhost", + "port": 4300, + "signalMappingId": "default_CDS_mapping" +} +---- ++ +. How to map an OperatorFabric signal key footnote:[currently, that means a severity, but in the future it could also be a process id, or anything identifying the signal to be played] to a signal (sound, light) on the external system -* For each user, which device to use - - - -//TODO Schema 1-n deviceConf, userConf, signalMapping - -//TODO rename OpFab signal key - -//TODO schema web-ui external devices & API - - -The external devices system is - -//TODO Explain Device vs DeviceConfiguration (realtime) - - - -//TODO Explain the interface that a driver should implement (add javadoc) + Exceptions? - - ++ +.signalMapping +[source,json,] +---- +{ + "id": "default_CDS_mapping", + "supportedSignals": { + "ALARM": 1, + "ACTION": 2, + "COMPLIANT": 3, + "INFORMATION": 4 + } +} +---- + +This means that a single physical device allowing 2 different sets of sounds to be played (for example one set for desk +A and another set for desk B) would be represented as two different device configurations, with different ids. + +.Device configurations +[source,json,] +---- +[{ + "id": "CDS_A", + "host": "localhost", + "port": 4300, + "signalMappingId": "mapping_A" +}, +{ + "id": "CDS_B", + "host": "localhost", + "port": 4300, + "signalMappingId": "mapping_B" +}] +---- + +.Signal mappings +[source,json,] +---- +[{ + "id": "mapping_A", + "supportedSignals": { + "ALARM": 1, + "ACTION": 2, + "COMPLIANT": 3, + "INFORMATION": 4 + } +}, +{ + "id": "mapping_B", + "supportedSignals": { + "ALARM": 5, + "ACTION": 6, + "COMPLIANT": 7, + "INFORMATION": 8 + } +}] +---- + +NOTE: The signalMapping object is built as a Map with String keys (rather than the Severity enum or any otherwise +constrained type) because there is a strong possibility that in the future we might want to map something other than +severities. + +Please see the https://opfab.github.io/documentation/current/api/external-devices/[API documentation] for details. + +NOTE: There is a `Device` object distinct from `DeviceConfiguration` because the latter represents static information +about how to reach a device, while the former contains information about the actual connexion and its status. +For example, this is why the device configuration contains a `host` (which can be a hostname) while the device +has a `resolvedAddress`. +As a result, they are managed through separate endpoints, which might also make things easier if we need to secure +them differently (some people might be allowed to connect/disconnect devices but not change their configuration). == Configuration - - - == Connexion Management OperatorFabric doesn't automatically attempt to connect to configured external devices on start up as they might not @@ -84,11 +189,13 @@ of the actual activation. However, if a notification is received by the external devices service that needs to be passed on to a device that is configured but not connected yet, the connection will be performed automatically. +== Configuration Management - -//TODO No checks on existing related resources when creation (imposes order in creation, makes init more complex) -//but removal when deletion (same as users/groups/perimeters) - +In coherence with the way Entities, Perimeters, Users and Groups are managed, SignalMapping, UserConfiguration and +DeviceConfiguration resources can be deleted even if other resources link to them. +For example, if a device configuration lists `someMapping` as its `signalMappingId` and a DELETE request is sent +on `someMapping`, the deletion will be performed and return a 200 Success, and the device will have a `null` +`signalMappingId`. == Drivers @@ -97,4 +204,30 @@ the https://en.wikipedia.org/wiki/Modbus[Modbus protocol]. === Modbus Driver -//TODO Explain no response but ok because watchdog \ No newline at end of file +The Modbus driver is based on the https://github.com/kochedykov/jlibmodbus[jlibmodbus] library to create a +`ModbusMaster` for each device and then send requests through it using the +https://github.com/kochedykov/jlibmodbus/blob/master/src/com/intelligt/modbus/jlibmodbus/msg/request/WriteSingleRegisterRequest.java[WriteSingleRegisterRequest] +object. + +We are currently using the "BROADCAST" mode, which (at least in the jlibmodbus implementation) means that the Modbus +master doesn't expect any response to its requests (which makes sense because if there really are several clients +responding to the broadcast, ) +This is mitigated by the fact that if watchdog signals are enabled, the external devices will be able to detect that +they are not receiving signals correctly. +In the future, it could be interesting to switch to the TCP default so OperatorFabric can be informed of any exception +in the processing of the request, allowing for example to give a more meaningful connection status +(see https://github.com/opfab/operatorfabric-core/issues/2294[#2294]) + +=== Adding new drivers + +New drivers should implement the `ExternalDeviceDriver` interface, and a corresponding factory implementing the +`ExternalDeviceDriverFactory` interface should be created with it. + +The idea is that in the future, using dependency injection, Spring should be able to pick up any factory on the classpath implementing +the correct interface. + +NOTE: `ExternalDeviceDriver`, `ExternalDeviceDriverFactory` and the accompanying custom exceptions should be made +available as a jar on Maven Central if we want to allow project users to provide their own drivers. + +NOTE: If several drivers need to be used on a given OperatorFabric instance at the same time, we will need to introduce +a device type in the deviceConfiguration object. \ No newline at end of file diff --git a/src/docs/asciidoc/reference_doc/template_description.adoc b/src/docs/asciidoc/reference_doc/template_description.adoc index bf7520361b..b6fc6773c5 100644 --- a/src/docs/asciidoc/reference_doc/template_description.adoc +++ b/src/docs/asciidoc/reference_doc/template_description.adoc @@ -570,4 +570,22 @@ For example: include::../../../test/resources/bundles/defaultProcess_V1/template/chart.handlebars[tag=templateGateway.redirectToBusinessMenu_example] ---- -This can be useful to pass context from the card to the business application. \ No newline at end of file +This can be useful to pass context from the card to the business application. + +=== Get list of all entities + +To have the list of all entities in OperatorFabric, you can call the javascript function _templateGateway.getAllEntities()_. +The function returns an array of entity object : + +Entity object has the following fields : + +- 'id' : id of the entity +- 'name' : name of the entity +- 'description' : description of the entity +- 'entityAllowedToSendCard' : boolean indicating whether the entity is allowed to send card or not +- 'parents' : list of parent entities + +=== Get information about an entity + +To have information about an entity in particular, you can call the javascript function _templateGateway.getEntity(entityId)_. +The function returns an entity object whose fields are mentioned above. \ No newline at end of file diff --git a/src/docs/asciidoc/reference_doc/users_management.adoc b/src/docs/asciidoc/reference_doc/users_management.adoc index d730b5b0bd..641dd5b0d4 100644 --- a/src/docs/asciidoc/reference_doc/users_management.adoc +++ b/src/docs/asciidoc/reference_doc/users_management.adoc @@ -40,6 +40,8 @@ The access to this service has to be authorized, in the `OAuth2` service used by NOTE: User login must be lowercase. Otherwise, it will be converted to lowercase before saving to the database. +WARNING: Resource identifiers such as login, group id, entity id and perimeter id must only contain the following characters: letters, _, - or digits. + ==== Automated user creation diff --git a/src/docs/asciidoc/resources/index.adoc b/src/docs/asciidoc/resources/index.adoc index c53b24ffbe..fcacc8416c 100644 --- a/src/docs/asciidoc/resources/index.adoc +++ b/src/docs/asciidoc/resources/index.adoc @@ -10,8 +10,6 @@ = Resources -include::mock_pipeline.adoc[leveloffset=+1] - include::jar_publication.adoc[leveloffset=+1] include::migration_guide.adoc[leveloffset=+1] diff --git a/src/main/docker/java-config-docker-entrypoint.sh b/src/main/docker/java-config-docker-entrypoint.sh index cbd8b515ae..e649c2f397 100755 --- a/src/main/docker/java-config-docker-entrypoint.sh +++ b/src/main/docker/java-config-docker-entrypoint.sh @@ -13,5 +13,5 @@ cp $JAVA_HOME/jre/lib/security/cacerts /tmp chmod u+w /tmp/cacerts ./add-certificates.sh /certificates_to_add /tmp/cacerts -java -agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n -Djavax.net.ssl.trustStore=/tmp/cacerts -Djava.security.egd=file:/dev/./urandom $JAVA_OPTIONS -jar /app.jar +exec java -agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n -Djavax.net.ssl.trustStore=/tmp/cacerts -Djava.security.egd=file:/dev/./urandom $JAVA_OPTIONS -jar /app.jar diff --git a/src/test/api/karate/admin/loggersApi.feature b/src/test/api/karate/admin/loggersApi.feature new file mode 100644 index 0000000000..c2b9c08fe5 --- /dev/null +++ b/src/test/api/karate/admin/loggersApi.feature @@ -0,0 +1,124 @@ +Feature: Log Level Access + + Background: + #Getting token for admin and operator1 user calling getToken.feature + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'user_test_api_1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def businessConfigLoggersUrl = opfabBusinessConfigUrl + 'actuator/loggers/org.opfab' + * def userLoggersUrl = opfabUserUrl + 'actuator/loggers/org.opfab' + * def cardsPublicationLoggersUrl = opfabCardsPublicationUrl + 'actuator/loggers/org.opfab' + * def cardsConsultationLoggersUrl = opfabCardsConsultationUrl + 'actuator/loggers/org.opfab' + * def externalDevicesLoggersUrl = opfabExternalDevicesUrl + 'actuator/loggers/org.opfab' + + + + * def info_level = + """ + { + "configuredLevel": "INFO" + } + """ + + * def debug_level = + """ + { + "configuredLevel": "DEBUG" + } + """ + + Scenario Outline: get logging level is restricted to admin user + + Given url + And header Authorization = 'Bearer ' + + When method + Then status + + Examples: + | url | method | token | expected | + | businessConfigLoggersUrl | get | authTokenAsTSO | 403 | + | businessConfigLoggersUrl | get | authToken | 200 | + | userLoggersUrl | get | authTokenAsTSO | 403 | + | userLoggersUrl | get | authToken | 200 | + | cardsPublicationLoggersUrl | get | authTokenAsTSO | 403 | + | cardsPublicationLoggersUrl | get | authToken | 200 | + | cardsConsultationLoggersUrl | get | authTokenAsTSO | 403 | + | cardsConsultationLoggersUrl | get | authToken | 200 | + | externalDevicesLoggersUrl | get | authTokenAsTSO | 403 | + | externalDevicesLoggersUrl | get | authToken | 200 | + + + + Scenario Outline: set logging level to DEBUG is restricted to admin user + + Given url + And header Authorization = 'Bearer ' + + And request debug_level + When method + Then status + + Examples: + | url | method | token | expected | + | businessConfigLoggersUrl | post | authTokenAsTSO | 403 | + | businessConfigLoggersUrl | post | authToken | 204 | + | userLoggersUrl | post | authTokenAsTSO | 403 | + | userLoggersUrl | post | authToken | 204 | + | cardsPublicationLoggersUrl | post | authTokenAsTSO | 403 | + | cardsPublicationLoggersUrl | post | authToken | 204 | + | cardsConsultationLoggersUrl | post | authTokenAsTSO | 403 | + | cardsConsultationLoggersUrl | post | authToken | 204 | + | externalDevicesLoggersUrl | post | authTokenAsTSO | 403 | + | externalDevicesLoggersUrl | post | authToken | 204 | + + Scenario Outline: check logging level is 'DEBUG' as admin + + Given url + And header Authorization = 'Bearer ' + authToken + When method get + Then status + And match response.configuredLevel == 'DEBUG' + + Examples: + | url | expected | + | businessConfigLoggersUrl | 200 | + | userLoggersUrl | 200 | + | cardsPublicationLoggersUrl | 200 | + | cardsConsultationLoggersUrl | 200 | + | externalDevicesLoggersUrl | 200 | + + + + Scenario Outline: set logging level to INFO as admin + + Given url + And header Authorization = 'Bearer ' + authToken + And request info_level + When method post + Then status + + Examples: + | url | expected | + | businessConfigLoggersUrl | 204 | + | userLoggersUrl | 204 | + | cardsPublicationLoggersUrl | 204 | + | cardsConsultationLoggersUrl | 204 | + | externalDevicesLoggersUrl | 204 | + + Scenario Outline: get logging level is 'INFO' as admin + + Given url + And header Authorization = 'Bearer ' + authToken + When method get + Then status + And match response.configuredLevel == 'INFO' + + Examples: + | url | expected | + | businessConfigLoggersUrl | 200 | + | userLoggersUrl | 200 | + | cardsPublicationLoggersUrl | 200 | + | cardsConsultationLoggersUrl | 200 | + | externalDevicesLoggersUrl | 200 | + diff --git a/src/test/api/karate/adminTests.txt b/src/test/api/karate/adminTests.txt index d7e4081ef8..08f72da62c 100644 --- a/src/test/api/karate/adminTests.txt +++ b/src/test/api/karate/adminTests.txt @@ -1 +1,3 @@ -admin/getPrometheusMonitoring.feature \ \ No newline at end of file + admin/getPrometheusMonitoring.feature \ + admin/loggersApi.feature \ + diff --git a/src/test/api/karate/externalDevicesTests.txt b/src/test/api/karate/externalDevicesTests.txt index f23c81adb4..ca2469c08c 100644 --- a/src/test/api/karate/externalDevicesTests.txt +++ b/src/test/api/karate/externalDevicesTests.txt @@ -1,2 +1,10 @@ - externaldevices/manageDeviceConfig.feature \ + externaldevices/fetchDeviceConfig.feature \ + externaldevices/fetchUserConfig.feature \ + externaldevices/fetchSignalConfig.feature \ + externaldevices/createDeviceConfig.feature \ + externaldevices/deleteDeviceConfig.feature \ + externaldevices/createUserConfig.feature \ + externaldevices/deleteUserConfig.feature \ + externaldevices/createSignalConfig.feature \ + externaldevices/deleteSignalConfig.feature \ externaldevices/sendNotification.feature \ \ No newline at end of file diff --git a/src/test/api/karate/externaldevices/manageDeviceConfig.feature b/src/test/api/karate/externaldevices/createDeviceConfig.feature similarity index 81% rename from src/test/api/karate/externaldevices/manageDeviceConfig.feature rename to src/test/api/karate/externaldevices/createDeviceConfig.feature index 793a38bf0c..ed5b03864e 100644 --- a/src/test/api/karate/externaldevices/manageDeviceConfig.feature +++ b/src/test/api/karate/externaldevices/createDeviceConfig.feature @@ -1,4 +1,4 @@ -Feature: Device Configuration Management +Feature: Device Configuration Management (Create) Background: # Get admin token @@ -11,7 +11,7 @@ Feature: Device Configuration Management * def deviceConfigEndpoint = 'externaldevices/configurations/devices' - Scenario: Create device with correct configuration + Scenario: Create deviceConfiguration with correct configuration * def configuration = read("resources/deviceConfigurations/CDS_5.json") @@ -22,7 +22,7 @@ Feature: Device Configuration Management When method post Then status 201 - Scenario: Create device with correct configuration but duplicate id + Scenario: Create deviceConfiguration with correct configuration but duplicate id * def configuration = read("resources/deviceConfigurations/duplicate_CDS_5.json") @@ -33,7 +33,7 @@ Feature: Device Configuration Management When method post Then status 400 - Scenario: Create device with incorrect configuration + Scenario: Create deviceConfiguration with incorrect configuration * def configuration = read("resources/deviceConfigurations/broken_config.json") @@ -44,7 +44,7 @@ Feature: Device Configuration Management When method post Then status 400 - Scenario: Create device without authentication + Scenario: Create deviceConfiguration without authentication * def configuration = read("resources/deviceConfigurations/CDS_5.json") @@ -54,8 +54,7 @@ Feature: Device Configuration Management When method post Then status 401 - - Scenario: Create device without admin role + Scenario: Create deviceConfiguration without admin role * def configuration = read("resources/deviceConfigurations/CDS_5.json") @@ -67,4 +66,3 @@ Feature: Device Configuration Management Then status 403 - diff --git a/src/test/api/karate/externaldevices/createSignalConfig.feature b/src/test/api/karate/externaldevices/createSignalConfig.feature new file mode 100644 index 0000000000..689997be9f --- /dev/null +++ b/src/test/api/karate/externaldevices/createSignalConfig.feature @@ -0,0 +1,68 @@ +Feature: Signal Configuration Management (Create) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def signalConfigEndpoint = 'externaldevices/configurations/signals' + + Scenario: Create signalMapping with correct configuration + + * def configuration = read("resources/signalMappings/new_signal_mapping.json") + + # Push configuration + Given url opfabUrl + signalConfigEndpoint + And header Authorization = 'Bearer ' + authToken + And request configuration + When method post + Then status 201 + + Scenario: Create signalMapping with correct configuration but duplicate id + + * def configuration = read("resources/signalMappings/duplicate_signal_mapping.json") + + # Push configuration + Given url opfabUrl + signalConfigEndpoint + And header Authorization = 'Bearer ' + authToken + And request configuration + When method post + Then status 400 + + Scenario: Create signalMapping with incorrect configuration + + * def configuration = read("resources/signalMappings/broken_signal_mapping.json") + + # Push configuration + Given url opfabUrl + signalConfigEndpoint + And header Authorization = 'Bearer ' + authToken + And request configuration + When method post + Then status 400 + + Scenario: Create signalMapping without authentication + + * def configuration = read("resources/signalMappings/new_signal_mapping.json") + + # Push configuration + Given url opfabUrl + signalConfigEndpoint + And request configuration + When method post + Then status 401 + + Scenario: Create signalMapping without admin role + + * def configuration = read("resources/signalMappings/new_signal_mapping.json") + + # Push configuration + Given url opfabUrl + signalConfigEndpoint + And header Authorization = 'Bearer ' + authTokenAsTSO + And request configuration + When method post + Then status 403 + + diff --git a/src/test/api/karate/externaldevices/createUserConfig.feature b/src/test/api/karate/externaldevices/createUserConfig.feature new file mode 100644 index 0000000000..61c8a9e59d --- /dev/null +++ b/src/test/api/karate/externaldevices/createUserConfig.feature @@ -0,0 +1,68 @@ +Feature: User Configuration Management (Create) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def userConfigEndpoint = 'externaldevices/configurations/users' + + Scenario: Create userConfiguration with correct configuration + + * def configuration = read("resources/userConfigurations/operator5.json") + + # Push configuration + Given url opfabUrl + userConfigEndpoint + And header Authorization = 'Bearer ' + authToken + And request configuration + When method post + Then status 201 + + Scenario: Create userConfiguration with correct configuration but duplicate id + + * def configuration = read("resources/userConfigurations/duplicate_operator1.json") + + # Push configuration + Given url opfabUrl + userConfigEndpoint + And header Authorization = 'Bearer ' + authToken + And request configuration + When method post + Then status 400 + + Scenario: Create userConfiguration with incorrect configuration + + * def configuration = read("resources/userConfigurations/broken_config.json") + + # Push configuration + Given url opfabUrl + userConfigEndpoint + And header Authorization = 'Bearer ' + authToken + And request configuration + When method post + Then status 400 + + Scenario: Create userConfiguration without authentication + + * def configuration = read("resources/userConfigurations/operator5.json") + + # Push configuration + Given url opfabUrl + userConfigEndpoint + And request configuration + When method post + Then status 401 + + Scenario: Create userConfiguration without admin role + + * def configuration = read("resources/userConfigurations/operator5.json") + + # Push configuration + Given url opfabUrl + userConfigEndpoint + And header Authorization = 'Bearer ' + authTokenAsTSO + And request configuration + When method post + Then status 403 + + diff --git a/src/test/api/karate/externaldevices/deleteDeviceConfig.feature b/src/test/api/karate/externaldevices/deleteDeviceConfig.feature new file mode 100644 index 0000000000..c292964ad4 --- /dev/null +++ b/src/test/api/karate/externaldevices/deleteDeviceConfig.feature @@ -0,0 +1,41 @@ +Feature: Device Configuration Management (Delete) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def deviceConfigEndpoint = 'externaldevices/configurations/devices' + + Scenario: Delete existing deviceConfiguration + + Given url opfabUrl + deviceConfigEndpoint + "/CDS_3" + And header Authorization = 'Bearer ' + authToken + When method delete + Then status 200 + + Scenario: Attempt to delete deviceConfiguration that doesn't exist + + Given url opfabUrl + deviceConfigEndpoint + "/CDS_that_doesnt_exist" + And header Authorization = 'Bearer ' + authToken + When method delete + Then status 404 + + Scenario: Delete deviceConfiguration without authentication + + Given url opfabUrl + deviceConfigEndpoint + "/CDS_3" + When method delete + Then status 401 + + Scenario: Delete deviceConfiguration without admin role + + Given url opfabUrl + deviceConfigEndpoint + "/CDS_3" + And header Authorization = 'Bearer ' + authTokenAsTSO + When method delete + Then status 403 + + diff --git a/src/test/api/karate/externaldevices/deleteSignalConfig.feature b/src/test/api/karate/externaldevices/deleteSignalConfig.feature new file mode 100644 index 0000000000..400c3398a3 --- /dev/null +++ b/src/test/api/karate/externaldevices/deleteSignalConfig.feature @@ -0,0 +1,41 @@ +Feature: Signal Configuration Management (Delete) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def signalConfigEndpoint = 'externaldevices/configurations/signals' + + Scenario: Delete existing signalMapping + + Given url opfabUrl + signalConfigEndpoint + "/exotic_CDS_mapping" + And header Authorization = 'Bearer ' + authToken + When method delete + Then status 200 + + Scenario: Attempt to delete signalMapping that doesn't exist + + Given url opfabUrl + signalConfigEndpoint + "/mapping_that_doesnt_exist" + And header Authorization = 'Bearer ' + authToken + When method delete + Then status 404 + + Scenario: Delete signalMapping without authentication + + Given url opfabUrl + signalConfigEndpoint + "/exotic_CDS_mapping" + When method delete + Then status 401 + + Scenario: Delete signalMapping without admin role + + Given url opfabUrl + signalConfigEndpoint + "/exotic_CDS_mapping" + And header Authorization = 'Bearer ' + authTokenAsTSO + When method delete + Then status 403 + + diff --git a/src/test/api/karate/externaldevices/deleteUserConfig.feature b/src/test/api/karate/externaldevices/deleteUserConfig.feature new file mode 100644 index 0000000000..4c79706449 --- /dev/null +++ b/src/test/api/karate/externaldevices/deleteUserConfig.feature @@ -0,0 +1,41 @@ +Feature: User Configuration Management (Delete) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def userConfigEndpoint = 'externaldevices/configurations/users' + + Scenario: Delete existing userConfiguration + + Given url opfabUrl + userConfigEndpoint + "/operator2" + And header Authorization = 'Bearer ' + authToken + When method delete + Then status 200 + + Scenario: Attempt to delete userConfiguration that doesn't exist + + Given url opfabUrl + userConfigEndpoint + "/user_that_doesnt_exist" + And header Authorization = 'Bearer ' + authToken + When method delete + Then status 404 + + Scenario: Delete userConfiguration without authentication + + Given url opfabUrl + userConfigEndpoint + "/operator2" + When method delete + Then status 401 + + Scenario: Delete userConfiguration without admin role + + Given url opfabUrl + userConfigEndpoint + "/operator2" + And header Authorization = 'Bearer ' + authTokenAsTSO + When method delete + Then status 403 + + diff --git a/src/test/api/karate/externaldevices/fetchDeviceConfig.feature b/src/test/api/karate/externaldevices/fetchDeviceConfig.feature new file mode 100644 index 0000000000..a194057c26 --- /dev/null +++ b/src/test/api/karate/externaldevices/fetchDeviceConfig.feature @@ -0,0 +1,63 @@ +Feature: Device Configuration Management (Fetch) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def deviceConfigEndpoint = 'externaldevices/configurations/devices' + + Scenario: Fetch all deviceConfigurations + + Given url opfabUrl + deviceConfigEndpoint + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 200 + And match response == '#array' + + Scenario: Fetch existing deviceConfiguration + + Given url opfabUrl + deviceConfigEndpoint + '/CDS_1' + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 200 + And match response == { id: 'CDS_1', host: 'dummy-modbus-device_1', port: 4030, signalMappingId: 'default_CDS_mapping'} + + Scenario: Attempt to fetch deviceConfiguration that doesn't exist + + Given url opfabUrl + deviceConfigEndpoint + '/device_that_doesnt_exist' + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 404 + + Scenario: Fetch deviceConfigurations without authentication + + Given url opfabUrl + deviceConfigEndpoint + When method GET + Then status 401 + + Scenario: Fetch deviceConfigurations without admin role + + Given url opfabUrl + deviceConfigEndpoint + And header Authorization = 'Bearer ' + authTokenAsTSO + When method GET + Then status 403 + + Scenario: Fetch a deviceConfiguration without authentication + + Given url opfabUrl + deviceConfigEndpoint + '/CDS_1' + When method GET + Then status 401 + + Scenario: Fetch a deviceConfiguration without admin role + + Given url opfabUrl + deviceConfigEndpoint + '/CDS_1' + And header Authorization = 'Bearer ' + authTokenAsTSO + When method GET + Then status 403 + + diff --git a/src/test/api/karate/externaldevices/fetchSignalConfig.feature b/src/test/api/karate/externaldevices/fetchSignalConfig.feature new file mode 100644 index 0000000000..a7f9c72b7f --- /dev/null +++ b/src/test/api/karate/externaldevices/fetchSignalConfig.feature @@ -0,0 +1,63 @@ +Feature: Signal Configuration Management (Fetch) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def signalConfigEndpoint = 'externaldevices/configurations/signals' + + Scenario: Fetch all signalMappings + + Given url opfabUrl + signalConfigEndpoint + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 200 + And match response == '#array' + + Scenario: Fetch existing signalMapping + + Given url opfabUrl + signalConfigEndpoint + '/default_CDS_mapping' + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 200 + And match response == {id:"default_CDS_mapping", supportedSignals:{ALARM:1,ACTION:2,COMPLIANT:3,INFORMATION:4}} + + Scenario: Attempt to fetch signalMapping that doesn't exist + + Given url opfabUrl + signalConfigEndpoint + '/mapping_that_doesnt_exist' + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 404 + + Scenario: Fetch signalMappings without authentication + + Given url opfabUrl + signalConfigEndpoint + When method GET + Then status 401 + + Scenario: Fetch signalMappings without admin role + + Given url opfabUrl + signalConfigEndpoint + And header Authorization = 'Bearer ' + authTokenAsTSO + When method GET + Then status 403 + + Scenario: Fetch a signalMapping without authentication + + Given url opfabUrl + signalConfigEndpoint + '/default_CDS_mapping' + When method GET + Then status 401 + + Scenario: Fetch a signalMapping without admin role + + Given url opfabUrl + signalConfigEndpoint + '/default_CDS_mapping' + And header Authorization = 'Bearer ' + authTokenAsTSO + When method GET + Then status 403 + + diff --git a/src/test/api/karate/externaldevices/fetchUserConfig.feature b/src/test/api/karate/externaldevices/fetchUserConfig.feature new file mode 100644 index 0000000000..ed4a7da827 --- /dev/null +++ b/src/test/api/karate/externaldevices/fetchUserConfig.feature @@ -0,0 +1,63 @@ +Feature: User Configuration Management (Fetch) + + Background: + # Get admin token + * def signIn = callonce read('../common/getToken.feature') { username: 'admin'} + * def authToken = signIn.authToken + + # Get TSO-operator + * def signInAsTSO = callonce read('../common/getToken.feature') { username: 'operator1'} + * def authTokenAsTSO = signInAsTSO.authToken + + * def userConfigEndpoint = 'externaldevices/configurations/users' + + Scenario: Fetch all userConfigurations + + Given url opfabUrl + userConfigEndpoint + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 200 + And match response == '#array' + + Scenario: Fetch existing userConfiguration + + Given url opfabUrl + userConfigEndpoint + '/operator1' + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 200 + And match response == { userLogin: 'operator1', externalDeviceId: 'CDS_1'} + + Scenario: Attempt to fetch userConfiguration that doesn't exist + + Given url opfabUrl + userConfigEndpoint + '/user_that_doesnt_exist' + And header Authorization = 'Bearer ' + authToken + When method GET + Then status 404 + + Scenario: Fetch userConfigurations without authentication + + Given url opfabUrl + userConfigEndpoint + When method GET + Then status 401 + + Scenario: Fetch userConfigurations without admin role + + Given url opfabUrl + userConfigEndpoint + And header Authorization = 'Bearer ' + authTokenAsTSO + When method GET + Then status 403 + + Scenario: Fetch a userConfiguration without authentication + + Given url opfabUrl + userConfigEndpoint + '/operator1' + When method GET + Then status 401 + + Scenario: Fetch a userConfiguration without admin role + + Given url opfabUrl + userConfigEndpoint + '/operator1' + And header Authorization = 'Bearer ' + authTokenAsTSO + When method GET + Then status 200 + + diff --git a/src/test/api/karate/externaldevices/resources/signalMappings/broken_signal_mapping.json b/src/test/api/karate/externaldevices/resources/signalMappings/broken_signal_mapping.json new file mode 100644 index 0000000000..5c0717c658 --- /dev/null +++ b/src/test/api/karate/externaldevices/resources/signalMappings/broken_signal_mapping.json @@ -0,0 +1,4 @@ +{ + "id":"default_CDS_mapping", + "supportedSignals": "this mapping is broken because supportedSignals is a string" +} \ No newline at end of file diff --git a/src/test/api/karate/externaldevices/resources/signalMappings/duplicate_signal_mapping.json b/src/test/api/karate/externaldevices/resources/signalMappings/duplicate_signal_mapping.json new file mode 100644 index 0000000000..91f81db5e2 --- /dev/null +++ b/src/test/api/karate/externaldevices/resources/signalMappings/duplicate_signal_mapping.json @@ -0,0 +1,9 @@ +{ + "id":"default_CDS_mapping", + "supportedSignals": { + "ALARM":5, + "ACTION":6, + "COMPLIANT":7, + "INFORMATION":8 + } +} \ No newline at end of file diff --git a/src/test/api/karate/externaldevices/resources/signalMappings/new_signal_mapping.json b/src/test/api/karate/externaldevices/resources/signalMappings/new_signal_mapping.json new file mode 100644 index 0000000000..cb634e5374 --- /dev/null +++ b/src/test/api/karate/externaldevices/resources/signalMappings/new_signal_mapping.json @@ -0,0 +1,9 @@ +{ + "id":"new_CDS_mapping", + "supportedSignals": { + "ALARM":1, + "ACTION":2, + "COMPLIANT":3, + "INFORMATION":4 + } +} \ No newline at end of file diff --git a/src/test/api/karate/externaldevices/resources/userConfigurations/broken_config.json b/src/test/api/karate/externaldevices/resources/userConfigurations/broken_config.json new file mode 100644 index 0000000000..989d00ccfc --- /dev/null +++ b/src/test/api/karate/externaldevices/resources/userConfigurations/broken_config.json @@ -0,0 +1,3 @@ +{ + "externalDeviceId": "this_config_is_no_good_because_it_doesnt_have_a_userLogin" +} \ No newline at end of file diff --git a/src/test/api/karate/externaldevices/resources/userConfigurations/duplicate_operator1.json b/src/test/api/karate/externaldevices/resources/userConfigurations/duplicate_operator1.json new file mode 100644 index 0000000000..2a4352b128 --- /dev/null +++ b/src/test/api/karate/externaldevices/resources/userConfigurations/duplicate_operator1.json @@ -0,0 +1,4 @@ +{ + "userLogin": "operator1", + "externalDeviceId": "CDS_2" +} \ No newline at end of file diff --git a/src/test/api/karate/externaldevices/resources/userConfigurations/operator5.json b/src/test/api/karate/externaldevices/resources/userConfigurations/operator5.json new file mode 100644 index 0000000000..8eea481cdb --- /dev/null +++ b/src/test/api/karate/externaldevices/resources/userConfigurations/operator5.json @@ -0,0 +1,4 @@ +{ + "userLogin": "operator5", + "externalDeviceId": "CDS_1" +} \ No newline at end of file diff --git a/src/test/api/karate/karate-config.js b/src/test/api/karate/karate-config.js index f46934e6a6..de384e4e21 100644 --- a/src/test/api/karate/karate-config.js +++ b/src/test/api/karate/karate-config.js @@ -17,7 +17,9 @@ function() { opfabUserUrl: opfab_server +":2103/", opfabBusinessConfigUrl: opfab_server +":2100/", opfabCardsConsultationUrl: opfab_server +":2104/", - opfabCardsPublicationUrl: opfab_server +":2102/" + opfabCardsPublicationUrl: opfab_server +":2102/", + opfabExternalDevicesUrl: opfab_server +":2105/" + }; karate.logger.debug('url opfab :' + config.opfabUrl ); diff --git a/src/test/cypress/cypress/integration/1-RealTimeUsers.spec.js b/src/test/cypress/cypress/integration/1-RealTimeUsers.spec.js index 48165251db..510caefd5b 100644 --- a/src/test/cypress/cypress/integration/1-RealTimeUsers.spec.js +++ b/src/test/cypress/cypress/integration/1-RealTimeUsers.spec.js @@ -17,12 +17,8 @@ describe ('RealTimeUsersPage',()=>{ }) - it('Connection of operator1', ()=>{ - cy.loginOpFab('operator1','test'); - }) - - it('Connection of admin and check of Real time users screen', ()=> { - cy.loginOpFab('admin', 'test'); + it('Connection of operator3 and check of Real time users screen', ()=> { + cy.loginOpFab('operator3', 'test'); //click on user menu (top right of the screen) cy.get('#opfab-navbar-drop_user_menu').click(); diff --git a/src/test/cypress/cypress/integration/CardDetail.spec.js b/src/test/cypress/cypress/integration/CardDetail.spec.js index 5a9f344b39..f1141c6056 100644 --- a/src/test/cypress/cypress/integration/CardDetail.spec.js +++ b/src/test/cypress/cypress/integration/CardDetail.spec.js @@ -51,8 +51,12 @@ describe('Card detail', function () { cy.get("#templateGateway-isUserMemberOfAnEntityRequiredToRespond").contains("true"); cy.get("#templateGateway-getEntityUsedForUserResponse").contains(/^ENTITY1$/); cy.get("#templateGateway-getDisplayContext").contains(/^realtime$/); - - + cy.get("#templateGateway-getAllEntities").contains("entity[0]:id=ENTITY1,name=Control Room 1,description=Control Room 1,entityAllowedToSendCard=true,parents=ALLCONTROLROOMS"); + cy.get("#templateGateway-getAllEntities").contains("entity[1]:id=ENTITY2,name=Control Room 2,description=Control Room 2,entityAllowedToSendCard=true,parents=ALLCONTROLROOMS"); + cy.get("#templateGateway-getAllEntities").contains("entity[2]:id=ENTITY3,name=Control Room 3,description=Control Room 3,entityAllowedToSendCard=true,parents=ALLCONTROLROOMS"); + cy.get("#templateGateway-getAllEntities").contains("entity[3]:id=ALLCONTROLROOMS,name=All Control Rooms,description=All Control Rooms,entityAllowedToSendCard=false,parents="); + cy.get("#templateGateway-getAllEntities").contains("entity[4]:id=ENTITY4,name=IT Supervision Center,description=IT Supervision Center,entityAllowedToSendCard=true,parents="); + cy.get("#templateGateway-getEntity-ENTITY1").contains(/^ENTITY1,Control Room 1,Control Room 1,true,ALLCONTROLROOMS$/); cy.get("#screenSize").contains("md"); // see card in full screen diff --git a/src/test/cypress/cypress/integration/FeedTests.spec.js b/src/test/cypress/cypress/integration/FeedTests.spec.js index 79872dbedc..ad79620cb9 100644 --- a/src/test/cypress/cypress/integration/FeedTests.spec.js +++ b/src/test/cypress/cypress/integration/FeedTests.spec.js @@ -115,4 +115,10 @@ describe ('FeedScreen tests',function () { cy.get('of-card-details').should('not.exist'); }) + + it('Check card visibility by publish date when business period is after selected time range', function () { + cy.sendCard('cypress/feed/futureEvent.json'); + cy.loginOpFab('operator1','test'); + cy.get('of-light-card').should('have.length',1); + }) }) diff --git a/src/test/cypress/cypress/integration/Resilience.specs.js b/src/test/cypress/cypress/integration/Resilience.specs.js new file mode 100644 index 0000000000..e3cc7b10f8 --- /dev/null +++ b/src/test/cypress/cypress/integration/Resilience.specs.js @@ -0,0 +1,108 @@ +/* Copyright (c) 2021, RTE (http://www.rte-france.com) + * See AUTHORS.txt + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 + * This file is part of the OperatorFabric project. + */ + + + +describe ('Resilience tests',function () { + + before('Set up configuration', function () { + + // This can stay in a `before` block rather than `beforeEach` as long as the test does not change configuration + cy.resetUIConfigurationFiles(); + + cy.loadTestConf(); + + // Clean up existing cards + cy.deleteAllCards(); + + }); + + + + it('Check card reception after nginx restart ', function () { + + cy.loginOpFab('operator1','test'); + + cy.get('of-light-card').should('have.length',0); + + // Stop nginx + cy.exec('docker stop web-ui'); + + cy.wait(15000); + + // Check loading spinner is present + cy.get('#opfab-connecting-spinner'); + + // Start Nginx + cy.exec('docker start web-ui'); + + // Wait for subscription to be fully restored + cy.wait(20000); + + cy.send6TestCards(); + cy.get('of-light-card').should('have.length',6); + + // Check loading spinner is not present anymore + cy.get('#opfab-connecting-spinner').should('not.exist'); + + + }); + + it('Check card reception after rabbit restart ', function () { + + cy.loginOpFab('operator1','test'); + + cy.delete6TestCards(); + cy.get('of-light-card').should('have.length',0); + + // Restart rabbitMQ + cy.exec('docker restart rabbit'); + + cy.wait(10000); // Wait for rabbitMQ to be fully up + + cy.send6TestCards(); + cy.get('of-light-card').should('have.length',6); + + }); + + // the following test will only be relevant if using docker mode + // in dev mode it will execute but the cards-consultation services will not be restart + it('Check card reception when cards-consultation is restarted ', function () { + + cy.loginOpFab('operator1', 'test'); + + cy.delete6TestCards(); + cy.get('of-light-card').should('have.length', 0); + + // wait for subscription to be fully working + cy.wait(5000); + + cy.exec('docker stop cards-consultation',{failOnNonZeroExit: false}).then((result) => { + // only if docker stop works, so it will not be executed in dev mode + if (result.code === 0) { + + // Send 6 cards when cards-consultation servcie is down + cy.send6TestCards(); + + cy.exec('docker start cards-consultation'); + + cy.waitForOpfabToStart(); + + // wait for subscription to be fully restored + cy.wait(20000); + + cy.get('of-light-card').should('have.length', 6); + } + }) + + + }); + + +}) diff --git a/src/test/cypress/cypress/integration/ResponseCard.spec.js b/src/test/cypress/cypress/integration/ResponseCard.spec.js index fd01e6d514..cd425faef8 100644 --- a/src/test/cypress/cypress/integration/ResponseCard.spec.js +++ b/src/test/cypress/cypress/integration/ResponseCard.spec.js @@ -44,14 +44,14 @@ describe ('Response card tests',function () { // Response button is present cy.get('#opfab-card-details-btn-response'); - cy.get('#opfab-card-details-btn-response').should('have.text', 'SEND IMPACT'); + cy.get('#opfab-card-details-btn-response').should('have.text', 'SEND RESPONSE'); // Respond to the card cy.get('#question-choice1').click(); cy.get('#opfab-card-details-btn-response').click(); - // The label of the validate button must be "MODIFY IMPACT" now - cy.get('#opfab-card-details-btn-response').should('have.text', 'MODIFY IMPACT'); + // The label of the validate button must be "MODIFY RESPONSE" now + cy.get('#opfab-card-details-btn-response').should('have.text', 'MODIFY RESPONSE'); // See in the feed the fact that user has respond (icon) cy.get('#opfab-feed-lightcard-hasChildCardFromCurrentUserEntity'); @@ -71,7 +71,7 @@ describe ('Response card tests',function () { // Respond to the new card cy.get('#question-choice3').click(); - cy.get('#opfab-card-details-btn-response').should('have.text', 'SEND IMPACT'); + cy.get('#opfab-card-details-btn-response').should('have.text', 'SEND RESPONSE'); cy.get('#opfab-card-details-btn-response').click(); // See in the feed the fact that user has respond (icon) diff --git a/src/test/cypress/cypress/integration/SessionEnded.spec.js b/src/test/cypress/cypress/integration/SessionEnded.spec.js new file mode 100644 index 0000000000..7d8a6468bd --- /dev/null +++ b/src/test/cypress/cypress/integration/SessionEnded.spec.js @@ -0,0 +1,96 @@ +/* Copyright (c) 2022, RTE (http://www.rte-france.com) + * See AUTHORS.txt + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 + * This file is part of the OperatorFabric project. + */ + + +describe('Session ended test', function () { + + const user = 'operator1'; + + // Do not use the generic login feature as we + // need to launch cy.clock after cy.visit('') + const loginWithClock = function () { + + cy.visit('') + cy.clock(new Date()); + cy.get('#opfab-login').type('operator1') + cy.get('#opfab-password').type('test') + cy.get('#opfab-login-btn-submit').click(); + + //Wait for the app to finish initializing + cy.get('#opfab-cypress-loaded-check', {timeout: 15000}).should('have.text', 'true'); + + } + + before('Reset UI configuration file ', function () { + cy.resetUIConfigurationFiles(); + cy.deleteAllCards(); + cy.deleteAllSettings(); + }) + + + it('Checking session end after 10 hours ', () => { + + loginWithClock(); + cy.stubPlaySound(); + // go 1 hour in the future + cy.tick(1*60*60*1000); + + // The session is active + cy.get('#opfab-sessionEnd').should('not.exist'); + + // go 10 hour in the future + cy.tick(10*60*60*1000); + + // Session is closed + // check session end message + cy.get('#opfab-sessionEnd'); + + // no sound configured , sound shall not be activated + cy.get('@playSound').its('callCount').should('eq', 0); + + }) + + it('Checking sound when session end ', () => { + + cy.loginOpFab('operator1', 'test'); + cy.openOpfabSettings(); + + // set severity alarm to be notified by sound + cy.get('#opfab-checkbox-setting-form-alarm').click(); + cy.waitDefaultTime(); + // set no replay for sound + cy.get('#opfab-checkbox-setting-form-replay').click(); + cy.waitDefaultTime(); + + cy.logoutOpFab(); + loginWithClock(); + cy.stubPlaySound(); + cy.waitDefaultTime(); // wait for configuration load end (in SoundNotificationService.ts) + + // go 1 hour in the future + cy.tick(1*60*60*1000); + + // The session is active + cy.get('#opfab-sessionEnd').should('not.exist'); + + // go 10 hour in the future + cy.tick(10*60*60*1000); + + // Session is closed + // check session end message + cy.get('#opfab-sessionEnd'); + + //As one sound is configured , sound shall be activated + cy.get('@playSound').its('callCount').should('eq', 1); + + + }) + + +}) diff --git a/src/test/cypress/cypress/integration/SoundNotification.spec.js b/src/test/cypress/cypress/integration/SoundNotification.spec.js index c846054b28..3a5a2cc222 100644 --- a/src/test/cypress/cypress/integration/SoundNotification.spec.js +++ b/src/test/cypress/cypress/integration/SoundNotification.spec.js @@ -1,4 +1,4 @@ -/* Copyright (c) 2021, RTE (http://www.rte-france.com) +/* Copyright (c) 2021-2022, RTE (http://www.rte-france.com) * See AUTHORS.txt * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,19 +12,6 @@ describe('Sound notification test', function () { const user = 'operator1'; - const openSettings = function () { - cy.get('#opfab-navbar-drop_user_menu').click(); - cy.get("#opfab-navbar-right-menu-settings").click({force: true}); - } - - // Stub playSound method to catch when opfab send a sound - const stubPlaySound = function () { - cy.window() - .its('soundNotificationService') - .then((soundNotificationService) => { - cy.stub(soundNotificationService, 'playSound').as('playSound') - }) - } before('Reset UI configuration file ', function () { //cy.loadTestConf(); Avoid to launch it as it is time consuming @@ -36,8 +23,8 @@ describe('Sound notification test', function () { describe('Checking sound when receiving notification ', function () { it('Sound when receiving card ', () => { cy.loginOpFab(user, 'test'); - stubPlaySound(); - openSettings(); + cy.stubPlaySound(); + cy.openOpfabSettings(); // set severity alarm to be notified by sound cy.get('#opfab-checkbox-setting-form-alarm').click(); @@ -59,7 +46,7 @@ describe('Sound notification test', function () { // no new sound cy.get('@playSound').its('callCount').should('eq', 1); - openSettings(); + cy.openOpfabSettings(); // set severity alarm to NOT be notified by sound cy.get('#opfab-checkbox-setting-form-alarm').click(); @@ -73,7 +60,7 @@ describe('Sound notification test', function () { // No new sound cy.get('@playSound').its('callCount').should('eq', 1); - openSettings(); + cy.openOpfabSettings(); // set severity action to be notified by sound cy.get('#opfab-checkbox-setting-form-action').click(); @@ -88,7 +75,7 @@ describe('Sound notification test', function () { // New sound cy.get('@playSound').its('callCount').should('eq', 2); - openSettings(); + cy.openOpfabSettings(); // set severity information to be notified by sound cy.get('#opfab-checkbox-setting-form-information').click(); @@ -108,10 +95,10 @@ describe('Sound notification test', function () { it('Repeating sound when receiving card with default repeating interval ', () => { cy.delete6TestCards(); cy.loginOpFab(user, 'test'); - stubPlaySound(); + cy.stubPlaySound(); // Activate repeating sound (no need to click the checkbox because it is already checked, because of the default value set to true in web-ui.json) - openSettings(); + cy.openOpfabSettings(); cy.waitDefaultTime(); // Open the feed and send card @@ -149,10 +136,10 @@ describe('Sound notification test', function () { it('Repeating sound when receiving card with custom repeating interval ', () => { cy.delete6TestCards(); cy.loginOpFab(user, 'test'); - stubPlaySound(); + cy.stubPlaySound(); // Set repeating interval to 20 seconds - openSettings(); + cy.openOpfabSettings(); cy.get('#opfab-setting-replayInterval').clear(); cy.get('#opfab-setting-replayInterval').type('20'); cy.waitDefaultTime(); diff --git a/src/test/cypress/cypress/support/commands.js b/src/test/cypress/cypress/support/commands.js index 2f4aa1e870..55504f856b 100644 --- a/src/test/cypress/cypress/support/commands.js +++ b/src/test/cypress/cypress/support/commands.js @@ -1,4 +1,4 @@ -/* Copyright (c) 2021, RTE (http://www.rte-france.com) +/* Copyright (c) 2021-2022, RTE (http://www.rte-france.com) * See AUTHORS.txt * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -123,4 +123,23 @@ Cypress.Commands.add('deleteAllCards', () => { Cypress.Commands.add('deleteAllSettings', () => { cy.exec('cd .. && ./resources/deleteAllSettings.sh '+Cypress.env('host')); -}) \ No newline at end of file +}) + +Cypress.Commands.add('waitForOpfabToStart', () => { + cy.exec('cd ../../.. && ./bin/waitForOpfabToStart.sh '); +}) + +Cypress.Commands.add('openOpfabSettings', () => { + cy.get('#opfab-navbar-drop_user_menu').click(); + cy.get("#opfab-navbar-right-menu-settings").click({force: true}); +}) + + // Stub playSound method to catch when opfab send a sound +Cypress.Commands.add('stubPlaySound', () => { + cy.window() + .its('soundNotificationService') + .then((soundNotificationService) => { + cy.stub(soundNotificationService, 'playSound').as('playSound') + }) +}) + diff --git a/src/test/cypress/package-lock.json b/src/test/cypress/package-lock.json index 6917b17599..75c5e8c725 100644 --- a/src/test/cypress/package-lock.json +++ b/src/test/cypress/package-lock.json @@ -52,9 +52,9 @@ } }, "@types/node": { - "version": "14.18.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.0.tgz", - "integrity": "sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ==", + "version": "14.18.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.2.tgz", + "integrity": "sha512-fqtSN5xn/bBzDxMT77C1rJg6CsH/R49E7qsGuvdPJa20HtV5zSTuLJPNfnlyVH3wauKnkHdLggTVkOW/xP9oQg==", "dev": true }, "@types/sinonjs__fake-timers": { @@ -362,9 +362,9 @@ } }, "cypress": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.1.1.tgz", - "integrity": "sha512-yWcYD8SEQ8F3okFbRPqSDj5V0xhrZBT5QRIH+P1J2vYvtEmZ4KGciHE7LCcZZLILOrs7pg4WNCqkj/XRvReQlQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.2.0.tgz", + "integrity": "sha512-Jn26Tprhfzh/a66Sdj9SoaYlnNX6Mjfmj5PHu2a7l3YHXhrgmavM368wjCmgrxC6KHTOv9SpMQGhAJn+upDViA==", "dev": true, "requires": { "@cypress/request": "^2.88.10", diff --git a/src/test/cypress/package.json b/src/test/cypress/package.json index 9f7377590d..478efed110 100644 --- a/src/test/cypress/package.json +++ b/src/test/cypress/package.json @@ -9,7 +9,7 @@ "author": "", "license": "MPL-2.0", "devDependencies": { - "cypress": "9.1.1", + "cypress": "9.2.0", "cypress-terminal-report": "3.4.1" } } diff --git a/src/test/dummyModbusDevice/dummyModbusDevice.gradle b/src/test/dummyModbusDevice/dummyModbusDevice.gradle index d55b74e42c..9a788bb343 100755 --- a/src/test/dummyModbusDevice/dummyModbusDevice.gradle +++ b/src/test/dummyModbusDevice/dummyModbusDevice.gradle @@ -24,11 +24,9 @@ bootJar { docker { - if (project.version.toUpperCase().equals("SNAPSHOT")) - name "lfeoperatorfabric/of-dummy-modbus-device:SNAPSHOT" /* more information : https://vsupalov.com/docker-latest-tag/ */ - else - name "lfeoperatorfabric/of-dummy-modbus-device" - tags "latest", "${project.version}" + name "lfeoperatorfabric/of-dummy-modbus-device:${project.version}" + if (!project.version.equals("SNAPSHOT")) + tag "latest", "latest" labels (['project':"${project.group}"]) files( "build/libs") dockerfile file('src/main/docker/Dockerfile') diff --git a/src/test/dummyModbusDevice/src/main/bin/startDummy.sh b/src/test/dummyModbusDevice/src/main/bin/startDummy.sh index 26bedef427..51ba6e866d 100755 --- a/src/test/dummyModbusDevice/src/main/bin/startDummy.sh +++ b/src/test/dummyModbusDevice/src/main/bin/startDummy.sh @@ -1,3 +1,12 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021, RTE (http://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of the OperatorFabric project. applicationOptions="--spring.profiles.active=dev --spring.config.location=classpath:/application.yml,file:${OF_HOME}/config/dev/ --spring.config.name=common,dummy-modbus-device" projectBuildPath="src/test/dummyModbusDevice/build" diff --git a/src/test/dummyModbusDevice/src/main/bin/stopDummy.sh b/src/test/dummyModbusDevice/src/main/bin/stopDummy.sh new file mode 100755 index 0000000000..642fac5139 --- /dev/null +++ b/src/test/dummyModbusDevice/src/main/bin/stopDummy.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021, RTE (http://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of the OperatorFabric project. + +cd $OF_HOME + +projectBuildPath="src/test/dummyModbusDevice/build" +projectPidFilePath="$projectBuildPath/PIDFILE" +echo "##########################################################" + if [ -f "$projectPidFilePath" ]; then + pid=$(<"$projectPidFilePath") + echo "Stopping $1 (pid: $pid)" + if ! kill $pid > /dev/null 2>&1; then + echo "$1: could not send SIGTERM to process $pid" >&2 + fi + else + echo "'$projectPidFilePath' not found" + fi +echo "##########################################################" \ No newline at end of file diff --git a/src/test/dummyModbusDevice/src/main/java/org/opfab/dummy/modbusdevice/OwnDataHolder.java b/src/test/dummyModbusDevice/src/main/java/org/opfab/dummy/modbusdevice/OwnDataHolder.java index 8a73fedb14..3a70dbab65 100644 --- a/src/test/dummyModbusDevice/src/main/java/org/opfab/dummy/modbusdevice/OwnDataHolder.java +++ b/src/test/dummyModbusDevice/src/main/java/org/opfab/dummy/modbusdevice/OwnDataHolder.java @@ -12,10 +12,12 @@ import com.intelligt.modbus.jlibmodbus.data.DataHolder; import com.intelligt.modbus.jlibmodbus.exception.IllegalDataAddressException; import com.intelligt.modbus.jlibmodbus.exception.IllegalDataValueException; +import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; +@Slf4j public class OwnDataHolder extends DataHolder { final List modbusEventListenerList = new ArrayList<>(); @@ -33,34 +35,31 @@ public boolean removeEventListener(ModbusEventListener listener) { } @Override - public void writeHoldingRegister(int offset, int value) throws IllegalDataAddressException, IllegalDataValueException { + public void writeHoldingRegister(int offset, int value) { for (ModbusEventListener l : modbusEventListenerList) { l.onWriteToSingleHoldingRegister(offset, value); } - super.writeHoldingRegister(offset, value); + try { + super.writeHoldingRegister(offset, value); + } catch (IllegalDataAddressException e) { + log.error("Attempting write on register with illegal address {}",offset,e); + } catch (IllegalDataValueException e) { + log.error("Attempting write on register with illegal value {}",value,e); + } } @Override - public void writeHoldingRegisterRange(int offset, int[] range) throws IllegalDataAddressException, IllegalDataValueException { - for (ModbusEventListener l : modbusEventListenerList) { - l.onWriteToMultipleHoldingRegisters(offset, range.length, range); - } - super.writeHoldingRegisterRange(offset, range); + public void writeHoldingRegisterRange(int offset, int[] range) { + // Not needed for our tests } @Override - public void writeCoil(int offset, boolean value) throws IllegalDataAddressException, IllegalDataValueException { - for (ModbusEventListener l : modbusEventListenerList) { - l.onWriteToSingleCoil(offset, value); - } - super.writeCoil(offset, value); + public void writeCoil(int offset, boolean value) { + // Not needed for our tests } @Override - public void writeCoilRange(int offset, boolean[] range) throws IllegalDataAddressException, IllegalDataValueException { - for (ModbusEventListener l : modbusEventListenerList) { - l.onWriteToMultipleCoils(offset, range.length, range); - } - super.writeCoilRange(offset, range); + public void writeCoilRange(int offset, boolean[] range) { + // Not needed for our tests } } \ No newline at end of file diff --git a/src/test/dummyModbusDevice/src/main/resources/application.yml b/src/test/dummyModbusDevice/src/main/resources/application.yml index 872a611897..12aafd56d5 100755 --- a/src/test/dummyModbusDevice/src/main/resources/application.yml +++ b/src/test/dummyModbusDevice/src/main/resources/application.yml @@ -5,4 +5,5 @@ spring: web-application-type: NONE modbus_client: port: 4030 +logging.level.org.opfab: debug diff --git a/src/test/externalApp/externalApp.gradle b/src/test/externalApp/externalApp.gradle index 0396a4ebd1..a4e74ec75b 100755 --- a/src/test/externalApp/externalApp.gradle +++ b/src/test/externalApp/externalApp.gradle @@ -28,11 +28,10 @@ bootJar { docker { - if (project.version.toUpperCase().equals("SNAPSHOT")) - name "lfeoperatorfabric/of-external-app:${project.version.toUpperCase()}" /* more information : https://vsupalov.com/docker-latest-tag/ */ - else - name "lfeoperatorfabric/of-external-app" - tags "latest", "${project.version}" + name "lfeoperatorfabric/of-external-app:${project.version}" + + if (!project.version.equals("SNAPSHOT")) + tag "latest", "latest" labels (['project':"${project.group}"]) files( "build/libs") dockerfile file('src/main/docker/Dockerfile') diff --git a/src/test/resources/bundles/cypress/template/kitchenSink.handlebars b/src/test/resources/bundles/cypress/template/kitchenSink.handlebars index 1d22aa5129..0485090651 100644 --- a/src/test/resources/bundles/cypress/template/kitchenSink.handlebars +++ b/src/test/resources/bundles/cypress/template/kitchenSink.handlebars @@ -58,6 +58,25 @@ function loadData() { responses += templateGateway.getDisplayContext(); responses += ''; + responses += '
getAllEntities() : '; + templateGateway.getAllEntities().forEach((entity, i) => { + responses += '
entity[' + i + ']:' + 'id=' + entity.id + ','; + responses += 'name=' + entity.name + ','; + responses += 'description=' + entity.description + ','; + responses += 'entityAllowedToSendCard=' + entity.entityAllowedToSendCard + ','; + responses += 'parents=' + entity.parents; + } + ); + responses += '
'; + + responses += '
getEntity("ENTITY1") : '; + responses += templateGateway.getEntity('ENTITY1').id + ','; + responses += templateGateway.getEntity('ENTITY1').name + ','; + responses += templateGateway.getEntity('ENTITY1').description + ','; + responses += templateGateway.getEntity('ENTITY1').entityAllowedToSendCard + ','; + responses += templateGateway.getEntity('ENTITY1').parents; + responses += '
'; + templateGatewayResults.innerHTML = responses; } diff --git a/src/test/resources/bundles/defaultProcess_V1/config.json b/src/test/resources/bundles/defaultProcess_V1/config.json index a70ce1002a..481c00ea20 100644 --- a/src/test/resources/bundles/defaultProcess_V1/config.json +++ b/src/test/resources/bundles/defaultProcess_V1/config.json @@ -82,8 +82,8 @@ ], "acknowledgmentAllowed": "OnlyWhenResponseDisabledForUser", "type" : "INPROGRESS", - "validateAnswerButtonLabel" : "SEND IMPACT", - "modifyAnswerButtonLabel" : "MODIFY IMPACT" + "validateAnswerButtonLabel" : "SEND RESPONSE", + "modifyAnswerButtonLabel" : "MODIFY RESPONSE" }, "multipleOptionsResponseState": { "name": "Additional information required", diff --git a/src/test/resources/cards/cypress/feed/futureEvent.json b/src/test/resources/cards/cypress/feed/futureEvent.json new file mode 100644 index 0000000000..7c2826481d --- /dev/null +++ b/src/test/resources/cards/cypress/feed/futureEvent.json @@ -0,0 +1,16 @@ +{ + "publisher" : "publisher_test", + "processVersion" : "1", + "process" :"cypress", + "processInstanceId" : "kitchenSink", + "state": "kitchenSink", + "entityRecipients": ["ENTITY1","ENTITY2"], + "entitiesRequiredToRespond" : ["ENTITY1"], + "severity" : "INFORMATION", + "startDate" : ${current_date_in_milliseconds_from_epoch_plus_24hours}, + "endDate" : ${current_date_in_milliseconds_from_epoch_plus_48hours} , + "summary" : {"key" : "kitchenSink.title"}, + "title" : {"key" : "kitchenSink.title"}, + "data" : {"message":"test"}, + "timeSpans" : [] +} \ No newline at end of file diff --git a/src/test/resources/externalDevices/CDS_5.json b/src/test/resources/externalDevices/CDS_5.json deleted file mode 100644 index f347216520..0000000000 --- a/src/test/resources/externalDevices/CDS_5.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id" : "CDS_5", - "host" : "dummy-modbus-device", - "port" : 8080, - "signalMappingId": "default_CDS_mapping" -} \ No newline at end of file diff --git a/src/test/resources/externalDevices/README.adoc b/src/test/resources/externalDevices/README.adoc deleted file mode 100644 index 615e4715a1..0000000000 --- a/src/test/resources/externalDevices/README.adoc +++ /dev/null @@ -1,31 +0,0 @@ -= External devices README - -This file gives a few pointers and command to test the external devices service in conjunction with the dummy modbus -device. It's intended as a first draft of the documentation. - -== Running in docker mode - ----- -cd $OF_HOME/config/docker -./docker-compose.sh ----- - -== Running in dev mode - ----- -cd $OF_HOME/config/dev -./docker-compose.sh -cd $OF_HOME -./run_all.sh --services users,cards-consultation,cards-publication,businessconfig,external-devices start -cd $OF_HOME/src/test/dummyModbusDevice/src/main/bin -./startDummy.sh ----- - -== Test - ----- -cd $OF_HOME/src/test/resources/externalDevices -./testExternalDevices.sh ----- - -Look at logs to see evidence of communication. \ No newline at end of file diff --git a/src/test/resources/externalDevices/connectDevice.sh b/src/test/resources/externalDevices/connectDevice.sh deleted file mode 100755 index a529cef803..0000000000 --- a/src/test/resources/externalDevices/connectDevice.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2021, RTE (http://www.rte-france.com) -# See AUTHORS.txt -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# SPDX-License-Identifier: MPL-2.0 -# This file is part of the OperatorFabric project. - - -url=$2 -if [ -z $url ] -then - url="http://localhost" -fi -if [ -z $1 ] -then - echo "Usage connectDevice deviceId opfab_url" -else - source ../getToken.sh "admin" $url - echo "connect device $1 (url: $url)" - curl -v -X POST $url:2105/devices/$1/connect -H "Authorization: Bearer $token" -H "Content-type:application/json" -fi \ No newline at end of file diff --git a/src/test/resources/externalDevices/notification.json b/src/test/resources/externalDevices/notification.json deleted file mode 100644 index aa9e111c18..0000000000 --- a/src/test/resources/externalDevices/notification.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "signalId" : "INFORMATION" -} \ No newline at end of file diff --git a/src/test/resources/externalDevices/sendDeviceConfig.sh b/src/test/resources/externalDevices/sendDeviceConfig.sh deleted file mode 100755 index 6ca2f0ddbe..0000000000 --- a/src/test/resources/externalDevices/sendDeviceConfig.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2021, RTE (http://www.rte-france.com) -# See AUTHORS.txt -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# SPDX-License-Identifier: MPL-2.0 -# This file is part of the OperatorFabric project. - - -url=$2 -if [ -z $url ] -then - url="http://localhost" -fi -if [ -z $1 ] -then - echo "Usage sendDeviceConfig configFile opfab_url" -else - source ../getToken.sh "admin" $url - echo "send config file $1 (url: $url)" - curl -v -X POST $url:2105/configurations/devices/ -H "Authorization: Bearer $token" -H "Content-type:application/json" --data "$(envsubst <$1)" -fi \ No newline at end of file diff --git a/src/test/resources/externalDevices/sendNotification.sh b/src/test/resources/externalDevices/sendNotification.sh deleted file mode 100755 index 01a51d36a0..0000000000 --- a/src/test/resources/externalDevices/sendNotification.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2021, RTE (http://www.rte-france.com) -# See AUTHORS.txt -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# SPDX-License-Identifier: MPL-2.0 -# This file is part of the OperatorFabric project. - - -url=$2 -if [ -z $url ] -then - url="http://localhost" -fi -if [ -z $1 ] -then - echo "Usage sendNotification notificationFile opfab_url" -else - source ../getToken.sh "operator2" $url - echo "send notification file $1 (url: $url)" - #curl -v -X POST $url:2105/notifications/ -H "Authorization: Bearer $token" -H "Content-type:application/json" --data "$(envsubst <$1)" - curl -v -X POST $url:2002/externaldevices/notifications/ -H "Authorization: Bearer $token" -H "Content-type:application/json" --data "$(envsubst <$1)" -fi - diff --git a/src/test/resources/externalDevices/testExternalDevices.sh b/src/test/resources/externalDevices/testExternalDevices.sh deleted file mode 100755 index e67aa9bca1..0000000000 --- a/src/test/resources/externalDevices/testExternalDevices.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2021, RTE (http://www.rte-france.com) -# See AUTHORS.txt -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# SPDX-License-Identifier: MPL-2.0 -# This file is part of the OperatorFabric project. - - -url=$1 -if [ -z $url ] -then - url="http://localhost" -fi -#./sendDeviceConfig.sh CDS_5.json $url -./connectDevice.sh CDS_2 $url -./sendNotification.sh notification.json $url - -# TODO Devices doesn't seem to work without the trailing slash, how come? - diff --git a/ui/main/package-lock.json b/ui/main/package-lock.json index a18cb5939f..296c0cf021 100644 --- a/ui/main/package-lock.json +++ b/ui/main/package-lock.json @@ -296,12 +296,12 @@ } }, "@angular/cdk": { - "version": "12.2.13", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", - "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.1.1.tgz", + "integrity": "sha512-66PyWg+zKdxTe3b1pc1RduT8hsMs/hJ0aD0JX0pSEWVq7O0OJWJ5f0z+Mk03T9tAERA3NK1GifcKEDq5k7R2Zw==", "requires": { "parse5": "^5.0.0", - "tslib": "^2.2.0" + "tslib": "^2.3.0" } }, "@angular/cli": { @@ -1874,11 +1874,11 @@ "dev": true }, "@ng-bootstrap/ng-bootstrap": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-10.0.0.tgz", - "integrity": "sha512-Sz+QaxjuyJYJ+zyUbf0TevgcgVesCPQiiFiggEzxKjzY5R+Hvq3YgryLdXf2r/ryePL+C3FXCcmmKpTM5bfczQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-11.0.0.tgz", + "integrity": "sha512-qDnB0+jbpQ4wjXpM4NPRAtwmgTDUCjGavoeRDZHOvFfYvx/MBf1RTjZEqTJ1Yqq1pKP4BWpzxCgVTunfnpmsjA==", "requires": { - "tslib": "^2.1.0" + "tslib": "^2.3.0" } }, "@ngrx/effects": { @@ -2239,9 +2239,9 @@ } }, "@types/jasmine": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.2.tgz", - "integrity": "sha512-qs4xjVm4V/XjM6owGm/x6TNmhGl5iKX8dkTdsgdgl9oFnqgzxLepnS7rN9Tdo7kDmnFD/VEqKrW57cGD2odbEg==", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.3.tgz", + "integrity": "sha512-SWyMrjgdAUHNQmutvDcKablrJhkDLy4wunTme8oYLjKp41GnHGxMRXr2MQMvy/qy8H3LdzwQk9gH4hZ6T++H8g==", "dev": true }, "@types/jasminewd2": { @@ -2280,9 +2280,9 @@ } }, "@types/node": { - "version": "16.11.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.14.tgz", - "integrity": "sha512-mK6BKLpL0bG6v2CxHbm0ed6RcZrAtTHBTd/ZpnlVPVa3HkumsqLE4BC4u6TQ8D7pnrRbOU0am6epuALs+Ncnzw==", + "version": "16.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz", + "integrity": "sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng==", "dev": true }, "@types/parse-json": { @@ -2549,9 +2549,9 @@ } }, "ag-grid-community": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-26.2.0.tgz", - "integrity": "sha512-YkNQUJ7EsmXbwfSrT4sIBYp1MbFku+ebEiqKc4+YFVZ5+nniHFzvSomHzFqbbkOtEfb42Gmo8cMNvBz9aPnZsQ==" + "version": "26.2.1", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-26.2.1.tgz", + "integrity": "sha512-aChSGNdPkBda4BhOUUEAmAkRlIG7rFU8UTXx3NPStavrCOHKLDRV90djIKuiXfM6ONBqKmeqw2as0yuLnSN8dw==" }, "agent-base": { "version": "6.0.2", @@ -5335,12 +5335,6 @@ "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", "dev": true }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -5815,12 +5809,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, "istanbul-lib-instrument": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", @@ -5848,113 +5836,10 @@ } } }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, "jasmine-core": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.10.1.tgz", - "integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.0.0.tgz", + "integrity": "sha512-tq24OCqHElgU9KDpb/8O21r1IfotgjIzalfW9eCmRR40LZpvwXT68iariIyayMwi0m98RDt16aljdbwK0sBMmQ==", "dev": true }, "jasmine-marbles": { @@ -6231,19 +6116,6 @@ "which": "^1.2.1" } }, - "karma-coverage-istanbul-reporter": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", - "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^3.0.2", - "minimatch": "^3.0.4" - } - }, "karma-jasmine": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz", @@ -6520,6 +6392,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, + "optional": true, "requires": { "pify": "^4.0.1", "semver": "^5.6.0" @@ -7521,7 +7394,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "dev": true, + "optional": true }, "piscina": { "version": "3.1.0", @@ -9199,18 +9073,11 @@ } }, "rxjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", - "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.1.tgz", + "integrity": "sha512-KExVEeZWxMZnZhUZtsJcFwz8IvPvgu4G2Z2QyqjZQzUGr32KDYuSxrEYO4w3tFFNbfLozcrKUTvTPi+E9ywJkQ==", "requires": { - "tslib": "~2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" - } + "tslib": "^2.1.0" } }, "safe-buffer": { diff --git a/ui/main/package.json b/ui/main/package.json index 6344335ee9..0f69d16545 100755 --- a/ui/main/package.json +++ b/ui/main/package.json @@ -6,7 +6,7 @@ "start": "ng serve", "build": "ng build", "test": "ng test", - "headless": "ng test --browsers ChromeHeadless --watch=false --code-coverage", + "headless": "ng test --browsers ChromeHeadless --watch=false", "lint": "ng lint", "e2e": "ng e2e", "audit": "npm audit --json | $(npm bin)/npm-audit-html --output reports/report.html" @@ -14,7 +14,7 @@ "private": true, "dependencies": { "@angular/animations": "13.1.1", - "@angular/cdk": "12.2.13", + "@angular/cdk": "13.1.1", "@angular/common": "13.1.1", "@angular/compiler": "13.1.1", "@angular/core": "13.1.1", @@ -32,7 +32,7 @@ "@fullcalendar/daygrid": "5.10.0", "@fullcalendar/interaction": "5.10.0", "@fullcalendar/timegrid": "5.10.0", - "@ng-bootstrap/ng-bootstrap": "10.0.0", + "@ng-bootstrap/ng-bootstrap": "11.0.0", "@ngrx/effects": "13.0.2", "@ngrx/entity": "13.0.2", "@ngrx/router-store": "13.0.2", @@ -42,7 +42,7 @@ "@swimlane/ngx-charts": "19.2.0", "@types/jwt-decode": "2.2.1", "ag-grid-angular": "26.2.0", - "ag-grid-community": "26.2.0", + "ag-grid-community": "26.2.1", "angular-oauth2-oidc": "13.0.1", "angular2-multiselect-dropdown": "5.0.4", "bootstrap": "4.6.1", @@ -54,7 +54,7 @@ "moment": "2.29.1", "moment-timezone": "0.5.34", "ng-event-source": "1.0.14", - "rxjs": "7.4.0", + "rxjs": "7.5.1", "svg-pan-zoom": "3.6.1", "tslib": "2.3.1", "xlsx": "0.17.4" @@ -70,19 +70,18 @@ "@ngrx/store-devtools": "13.0.2", "@types/file-saver": "2.0.4", "@types/handlebars": "4.0.40", - "@types/jasmine": "3.10.2", + "@types/jasmine": "3.10.3", "@types/jasminewd2": "2.0.10", "@types/lodash": "4.14.178", "@types/moment": "2.13.0", - "@types/node": "16.11.14", + "@types/node": "16.11.19", "codelyzer": "6.0.2", - "jasmine-core": "3.10.1", + "jasmine-core": "4.0.0", "jasmine-marbles": "0.8.4", "jasmine-spec-reporter": "7.0.0", "jasmine-sse": "0.3.0", "karma": "6.3.9", "karma-chrome-launcher": "3.1.0", - "karma-coverage-istanbul-reporter": "3.0.3", "karma-jasmine": "4.0.1", "karma-jasmine-html-reporter": "1.7.0", "karma-junit-reporter": "2.0.1", diff --git a/ui/main/src/app/app.component.html b/ui/main/src/app/app.component.html index 8b3c0ffa52..c1896958e0 100644 --- a/ui/main/src/app/app.component.html +++ b/ui/main/src/app/app.component.html @@ -1,4 +1,4 @@ - + @@ -23,7 +23,7 @@
-
+
@@ -54,3 +54,13 @@
+ + + + + \ No newline at end of file diff --git a/ui/main/src/app/app.component.ts b/ui/main/src/app/app.component.ts index 4f2cb7fbc8..d3772e23af 100644 --- a/ui/main/src/app/app.component.ts +++ b/ui/main/src/app/app.component.ts @@ -1,4 +1,4 @@ -/* Copyright (c) 2018-2021, RTE (http://www.rte-france.com) +/* Copyright (c) 2018-2022, RTE (http://www.rte-france.com) * See AUTHORS.txt * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -10,7 +10,7 @@ import {Component, HostListener, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {Title} from '@angular/platform-browser'; -import {Store} from '@ngrx/store'; +import {Action, Store} from '@ngrx/store'; import {AppState} from '@ofStore/index'; import {AuthenticationService} from '@ofServices/authentication/authentication.service'; import {LoadConfigSuccess} from '@ofActions/config.actions'; @@ -32,6 +32,7 @@ import {Message, MessageLevel} from '@ofModel/message.model'; import {GroupsService} from '@ofServices/groups.service'; import {SoundNotificationService} from "@ofServices/sound-notification.service"; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {AuthenticationActionTypes, TryToLogOut} from '@ofStore/actions/authentication.actions'; class Alert { @@ -58,6 +59,7 @@ export class AppComponent implements OnInit { private modalRef: NgbModalRef; @ViewChild('noSound') noSoundPopupRef: TemplateRef; + @ViewChild('sessionEnd') sessionEndPopupRef: TemplateRef; /** * NB: I18nService is injected to trigger its constructor at application startup @@ -88,7 +90,7 @@ export class AppComponent implements OnInit { @HostListener('document:click', ['$event.target']) public onPageClick() { - this.soundNotificationService.clearOutstandingNotifications(); + this.soundNotificationService.clearOutstandingNotifications(); } @@ -164,6 +166,7 @@ export class AppComponent implements OnInit { this.loaded = true; this.reminderService.startService(identifier); this.activateSoundIfNotActivated(); + this.subscribeToSessionEnd(); }, error: catchError((err, caught) => { console.error('Error in application initialization', err); @@ -220,6 +223,20 @@ export class AppComponent implements OnInit { }); } + private subscribeToSessionEnd() { + this.actions$.pipe( + ofType(AuthenticationActionTypes.SessionExpired)).subscribe( () => { + this.soundNotificationService.handleSessionEnd(); + this.modalRef = this.modalService.open(this.sessionEndPopupRef, {centered: true, backdrop: 'static'}); + } + ); + } + + public logout() { + this.modalRef.close(); + this.store.dispatch(new TryToLogOut()); + } + private displayAlert(message: Message) { let className = 'opfab-alert-info'; switch (message.level) { diff --git a/ui/main/src/app/model/external-devices.model.ts b/ui/main/src/app/model/external-devices.model.ts index 74de1866d3..e7e4289d1a 100644 --- a/ui/main/src/app/model/external-devices.model.ts +++ b/ui/main/src/app/model/external-devices.model.ts @@ -12,4 +12,11 @@ export class Notification { constructor( readonly opfabSignalId: string) { } +} + +export class UserConfiguration { + public constructor( + readonly userLogin: string, + readonly externalDeviceId: string + ) {} } \ No newline at end of file diff --git a/ui/main/src/app/modules/admin/components/cell-renderers/entity-cell-renderer.component.ts b/ui/main/src/app/modules/admin/components/cell-renderers/entity-cell-renderer.component.ts index aa3c91b922..b8f30543dd 100644 --- a/ui/main/src/app/modules/admin/components/cell-renderers/entity-cell-renderer.component.ts +++ b/ui/main/src/app/modules/admin/components/cell-renderers/entity-cell-renderer.component.ts @@ -23,4 +23,8 @@ export class EntityCellRendererComponent extends ArrayCellRendererComponent { * */ itemType = AdminItemType.ENTITY; + + agInit(params: any): void { + super.agInit(params); + } } diff --git a/ui/main/src/app/modules/admin/components/cell-renderers/group-cell-renderer.component.ts b/ui/main/src/app/modules/admin/components/cell-renderers/group-cell-renderer.component.ts index 07b937cf2e..fc1ed13795 100644 --- a/ui/main/src/app/modules/admin/components/cell-renderers/group-cell-renderer.component.ts +++ b/ui/main/src/app/modules/admin/components/cell-renderers/group-cell-renderer.component.ts @@ -22,4 +22,8 @@ export class GroupCellRendererComponent extends ArrayCellRendererComponent { * */ itemType = AdminItemType.GROUP; + + agInit(params: any): void { + super.agInit(params); + } } diff --git a/ui/main/src/app/modules/admin/components/table/admin-table.directive.html b/ui/main/src/app/modules/admin/components/table/admin-table.directive.html index cbdcb1b5c0..f01f3f1586 100644 --- a/ui/main/src/app/modules/admin/components/table/admin-table.directive.html +++ b/ui/main/src/app/modules/admin/components/table/admin-table.directive.html @@ -16,6 +16,7 @@ [gridOptions]="gridOptions" [rowData]="rowData" class="opfab-ag-grid-theme" + (filterChanged)="onFilterChanged($event)" > diff --git a/ui/main/src/app/modules/admin/components/table/admin-table.directive.ts b/ui/main/src/app/modules/admin/components/table/admin-table.directive.ts index 5f7ee14281..32db1d4873 100644 --- a/ui/main/src/app/modules/admin/components/table/admin-table.directive.ts +++ b/ui/main/src/app/modules/admin/components/table/admin-table.directive.ts @@ -24,19 +24,27 @@ import {takeUntil} from 'rxjs/operators'; import {StateRightsCellRendererComponent} from '../cell-renderers/state-rights-cell-renderer.component'; import {ProcessesService} from "@ofServices/processes.service"; import {Process} from "@ofModel/processes.model"; +import {GroupsService} from "@ofServices/groups.service"; +import {Group} from "@ofModel/group.model"; +import {Entity} from "@ofModel/entity.model"; +import {EntitiesService} from "@ofServices/entities.service"; @Directive() @Injectable() export abstract class AdminTableDirective implements OnInit, OnDestroy { processesDefinition: Process[]; + groupsDefinition: Group[]; + entitiesDefinition: Entity[]; constructor( protected translateService: TranslateService, protected confirmationDialogService: ConfirmationDialogService, protected modalService: NgbModal, protected dataHandlingService: SharingService, - private processesService: ProcessesService) { + private processesService: ProcessesService, + private groupsService: GroupsService, + private entitiesService: EntitiesService) { this.processesDefinition = this.processesService.getAllProcesses(); this.gridOptions = { @@ -71,6 +79,70 @@ export abstract class AdminTableDirective implements OnInit, OnDestroy { autoHeight: true, flex: 4, }, + 'groupsColumn': { + sortable: true, + filter: "agTextColumnFilter", + filterParams: { + valueGetter: params => { + let text = ''; + params.data.groups.forEach(group => { + text += (this.groupsDefinition.filter(groupDefinition => group === groupDefinition.id) + .map(groupDefinition => groupDefinition.name) + ' '); + }); + return text; + } + }, + wrapText: true, + autoHeight: true, + flex: 4, + }, + 'entitiesColumn': { + sortable: true, + filter: "agTextColumnFilter", + filterParams: { + valueGetter: params => { + let text = ''; + params.data.entities.forEach(entity => { + text += (this.entitiesDefinition.filter(entityDefinition => entity === entityDefinition.id) + .map(entityDefinition => entityDefinition.name) + ' '); + }); + return text; + } + }, + wrapText: true, + autoHeight: true, + flex: 4, + }, + 'entityAllowedToSendCardColumn': { + sortable: true, + filter: "agTextColumnFilter", + filterParams: { + valueGetter: params => { + return params.data.entityAllowedToSendCard ? this.translateService.instant('admin.input.entity.true') + : this.translateService.instant('admin.input.entity.false'); + } + }, + wrapText: true, + autoHeight: true, + flex: 4, + }, + 'parentsColumn': { + sortable: true, + filter: "agTextColumnFilter", + filterParams: { + valueGetter: params => { + let text = ''; + params.data.parents.forEach(parent => { + text += (this.entitiesDefinition.filter(entityDefinition => parent === entityDefinition.id) + .map(entityDefinition => entityDefinition.name) + ' '); + }); + return text; + } + }, + wrapText: true, + autoHeight: true, + flex: 4, + }, 'stateRightsColumn': { sortable: false, filter: "agTextColumnFilter", @@ -98,6 +170,7 @@ export abstract class AdminTableDirective implements OnInit, OnDestroy { headerHeight: 70, suppressPaginationPanel: true, suppressHorizontalScroll: true, + popupParent: document.querySelector("body") }; // Defining a custom cellRenderer was necessary (instead of using onCellClicked & an inline cellRenderer) because of // the need to call a method from the parent component @@ -144,6 +217,11 @@ export abstract class AdminTableDirective implements OnInit, OnDestroy { return this.translateService.instant(this.i18NPrefix + headerIdentifier); } + onFilterChanged(event) { + this.page = 1; + this.gridApi.paginationGoToPage(0); + } + onGridReady(params) { this.gridApi = params.api; // Column definitions can't be managed in the constructor like the other grid options because they rely on the `fields` @@ -155,6 +233,8 @@ export abstract class AdminTableDirective implements OnInit, OnDestroy { .subscribe(pageSize => { this.gridApi.paginationSetPageSize(pageSize); }); + this.groupsDefinition = this.groupsService.getGroups(); + this.entitiesDefinition = this.entitiesService.getEntities(); } /** This function generates the ag-grid `ColumnDefs` for the grid from a list of fields @@ -177,9 +257,21 @@ export abstract class AdminTableDirective implements OnInit, OnDestroy { field: field.name }; - if (field.name === 'stateRights') + if ((this.tableType === AdminItemType.USER) && field.name === 'groups') + columnDef.type = 'groupsColumn'; + + if ((this.tableType === AdminItemType.USER) && (field.name === 'entities')) + columnDef.type = 'entitiesColumn'; + + if ((this.tableType === AdminItemType.PERIMETER) && (field.name === 'stateRights')) columnDef.type = 'stateRightsColumn'; + if ((this.tableType === AdminItemType.ENTITY) && (field.name === 'entityAllowedToSendCard')) + columnDef.type = 'entityAllowedToSendCardColumn'; + + if ((this.tableType === AdminItemType.ENTITY) && (field.name === 'parents')) + columnDef.type = 'parentsColumn'; + if (!!field.flex) columnDef['flex'] = field.flex; if (!!field.cellRendererName) columnDef['cellRenderer'] = field.cellRendererName; if (!!field.valueFormatter) { diff --git a/ui/main/src/app/modules/admin/components/table/entities-table.component.ts b/ui/main/src/app/modules/admin/components/table/entities-table.component.ts index d12b2ebe91..c0ab9f9274 100644 --- a/ui/main/src/app/modules/admin/components/table/entities-table.component.ts +++ b/ui/main/src/app/modules/admin/components/table/entities-table.component.ts @@ -21,7 +21,11 @@ export class EntitiesTableComponent extends AdminTableDirective implements OnIni tableType = AdminItemType.ENTITY; - fields = [new Field('id', 3), new Field('name', 3), new Field('description', 5), new Field('entityAllowedToSendCard', 3, null, this.translateValue), new Field('parents', 5, 'entityCellRenderer')]; + fields = [new Field('id', 3), + new Field('name', 3), + new Field('description', 5), + new Field('entityAllowedToSendCard', 3, null, this.translateValue), + new Field('parents', 5, 'entityCellRenderer')]; idField = 'id'; editModalComponent = EditEntityModalComponent; diff --git a/ui/main/src/app/modules/cards/components/card-details/card-details.component.ts b/ui/main/src/app/modules/cards/components/card-details/card-details.component.ts index 4f90c45afc..dd4f6c9ad9 100644 --- a/ui/main/src/app/modules/cards/components/card-details/card-details.component.ts +++ b/ui/main/src/app/modules/cards/components/card-details/card-details.component.ts @@ -39,6 +39,7 @@ export class CardDetailsComponent implements OnInit, OnDestroy { cardState: State; unsubscribe$: Subject = new Subject(); cardLoadingInProgress = false; + cardNotFound = false; currentSelectedCardId: string; protected _currentPath: string; @@ -54,6 +55,8 @@ export class CardDetailsComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.unsubscribe$)) .subscribe(([card, childCards]: [Card, Card[]]) => { if (!!card) { + this.cardNotFound = false; + this.businessconfigService.queryProcess(card.process, card.processVersion) .subscribe({ next: businessconfig => { @@ -82,9 +85,11 @@ export class CardDetailsComponent implements OnInit, OnDestroy { this.cardState = new State(); } }); + } else { + this.cardNotFound = true; + console.log(new Date().toISOString(), 'WARNING card not found.'); } }); - this.store.select(selectCurrentUrl) .pipe(takeUntil(this.unsubscribe$)) .subscribe(url => { @@ -94,7 +99,8 @@ export class CardDetailsComponent implements OnInit, OnDestroy { this._currentPath = urlParts[CURRENT_PAGE_INDEX]; } }); - this.checkForCardLoadingInProgressForMoreThanOneSecond(); + if(!this.cardNotFound) + this.checkForCardLoadingInProgressForMoreThanOneSecond(); } diff --git a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts index 8f15b55189..a47f1ced13 100644 --- a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts +++ b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts @@ -72,38 +72,6 @@ describe('CustomTimelineChartComponent', () => { }); - it('should test checkFollowClockTick && updateRealTimeDate functions with : ' + - 'an empty ticks list, ' + - 'a ticks list of moment with a length biggest than 5, ' + - 'followClockTick set to true', () => { - fixture.detectChanges(); - expect(component.checkFollowClockTick()).toBeFalsy(); - component.followClockTick = true; - component.updateRealTimeDate(); - component.xTicks = []; - component.xDomain = [0, 1]; - const tmp = moment(); - tmp.millisecond(0); - for (let i = 0; i < 6; i++) { - component.xTicks.push(tmp); - } - expect(component.checkFollowClockTick()).toBeTruthy(); - expect(component).toBeTruthy(); - }); - - it('should test checkFollowClockTick function with a ticks list ' + - 'of moment (next day) with a length biggest than 5', () => { - fixture.detectChanges(); - component.xTicks = []; - component.xDomain = [0, 1]; - const tmp = moment(); - tmp.add(1, 'day'); - for (let i = 0; i < 6; i++) { - component.xTicks.push(tmp); - } - expect(component.checkFollowClockTick()).toBeFalsy(); - expect(component).toBeTruthy(); - }); diff --git a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts index d9d0ae97c0..724a3e3447 100644 --- a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts +++ b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts @@ -41,6 +41,8 @@ import {takeUntil} from 'rxjs/operators'; import {getNextTimeForRepeating} from '@ofServices/reminder/reminderUtils'; import {NgbPopover} from '@ng-bootstrap/ng-bootstrap'; import {LightCardsFeedFilterService} from '@ofServices/lightcards/lightcards-feed-filter.service'; +import {FilterType} from '@ofModel/feed-filter.model'; +import {FilterService} from '@ofServices/lightcards/filter.service'; @Component({ @@ -152,7 +154,8 @@ export class CustomTimelineChartComponent extends BaseChartComponent implements private store: Store, private router: Router, @Inject(PLATFORM_ID) platformId: any, - private lightCardsFeedFilterService: LightCardsFeedFilterService) { + private lightCardsFeedFilterService: LightCardsFeedFilterService, + private filterService: FilterService) { super(chartElement, zone, cd, platformId); } @@ -164,7 +167,7 @@ export class CustomTimelineChartComponent extends BaseChartComponent implements } }); this.initGraph(); - this.updateRealTimeDate(); + this.updateRealtime(); this.initDataPipe(); this.updateDimensions(); // need to init here only for unit test , otherwise dims is null } @@ -192,31 +195,27 @@ export class CustomTimelineChartComponent extends BaseChartComponent implements * update the domain if check follow clock tick is true * Stop it when destroying component to avoid memory leak */ - updateRealTimeDate(): void { + updateRealtime(): void { this.xRealTimeLine = moment(); - if (this.followClockTick) { - if (this.checkFollowClockTick()) { - this.update(); - } - } + if (this.followClockTick) this.shiftTimeLineIfNecessary(); setTimeout(() => { - if (!this.isDestroyed) this.updateRealTimeDate(); + if (!this.isDestroyed) this.updateRealtime(); }, 1000); } - /** - * change domain start with the second tick value - * if moment is equal to the 4th tick return true - */ - checkFollowClockTick(): boolean { - if (this.xTicks && this.xTicks.length > 5) { - if (this.xTicks[4].valueOf() <= moment().millisecond(0).valueOf()) { + shiftTimeLineIfNecessary() { + if (this.xTicks) { + if (this.xTicks[10].valueOf() <= moment().valueOf()) { this.valueDomain = [this.xTicks[1].valueOf(), this.xDomain[1] + (this.xTicks[1] - this.xDomain[0])]; - return true; + this.filterService.updateFilter( + FilterType.BUSINESSDATE_FILTER, + true, + {start: this.valueDomain[0], end: this.valueDomain[1], domainId: this.domainId} + ); + this.update(); } } - return false; } /** diff --git a/ui/main/src/app/modules/logging/logging.component.ts b/ui/main/src/app/modules/logging/logging.component.ts index efc4c21439..fca07cba0e 100644 --- a/ui/main/src/app/modules/logging/logging.component.ts +++ b/ui/main/src/app/modules/logging/logging.component.ts @@ -305,7 +305,7 @@ export class LoggingComponent implements OnDestroy, OnInit { [representativeColumnName]: this.translateColumn(card.representative) }); }); - ExportService.exportJsonToExcelFile(exportArchiveData, 'Archive'); + ExportService.exportJsonToExcelFile(exportArchiveData, 'Logging'); }); } diff --git a/ui/main/src/app/modules/monitoring/components/monitoring-table/monitoring-table.component.ts b/ui/main/src/app/modules/monitoring/components/monitoring-table/monitoring-table.component.ts index 0599cfe1a7..cfddcf812f 100644 --- a/ui/main/src/app/modules/monitoring/components/monitoring-table/monitoring-table.component.ts +++ b/ui/main/src/app/modules/monitoring/components/monitoring-table/monitoring-table.component.ts @@ -176,7 +176,8 @@ export class MonitoringTableComponent implements OnChanges, OnDestroy { suppressPaginationPanel: true, suppressHorizontalScroll: true, columnDefs: this.columnDefs, - rowHeight: 45 + rowHeight: 45, + popupParent: document.querySelector("body") }; this.rowData$ = this.rowDataSubject.asObservable(); } diff --git a/ui/main/src/app/modules/monitoring/monitoring.component.ts b/ui/main/src/app/modules/monitoring/monitoring.component.ts index 0f97b98538..919c3cfcf8 100644 --- a/ui/main/src/app/modules/monitoring/monitoring.component.ts +++ b/ui/main/src/app/modules/monitoring/monitoring.component.ts @@ -10,7 +10,7 @@ import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {combineLatest, Observable, of, Subject} from 'rxjs'; import {LineOfMonitoringResult} from '@ofModel/line-of-monitoring-result.model'; -import {catchError, filter, map, takeUntil} from 'rxjs/operators'; +import {catchError, debounceTime, filter, map, takeUntil} from 'rxjs/operators'; import {LightCard} from '@ofModel/light-card.model'; import * as moment from 'moment'; import {I18n} from '@ofModel/i18n.model'; @@ -76,6 +76,7 @@ export class MonitoringComponent implements OnInit, OnDestroy { this.lightCardsStoreService.getLightCards() ] ).pipe( + debounceTime(0), // Add this to avoid ExpressionChangedAfterItHasBeenCheckedError so it waits for component init before processing takeUntil(this.unsubscribe$), // the filters are set by the monitoring filter and by the time line // so it generates two events , we need to wait until every filter is set diff --git a/ui/main/src/app/modules/settings/components/settings/settings.component.html b/ui/main/src/app/modules/settings/components/settings/settings.component.html index 7f9ed89a48..d2a981cc1c 100644 --- a/ui/main/src/app/modules/settings/components/settings/settings.component.html +++ b/ui/main/src/app/modules/settings/components/settings/settings.component.html @@ -21,7 +21,7 @@
- +
diff --git a/ui/main/src/app/modules/settings/components/settings/settings.component.ts b/ui/main/src/app/modules/settings/components/settings/settings.component.ts index 80646dca00..1eceb9f687 100644 --- a/ui/main/src/app/modules/settings/components/settings/settings.component.ts +++ b/ui/main/src/app/modules/settings/components/settings/settings.component.ts @@ -13,6 +13,9 @@ import {Component, OnInit} from '@angular/core'; import {Store} from '@ngrx/store'; import {AppState} from '@ofStore/index'; import {ConfigService} from '@ofServices/config.service'; +import {ExternalDevicesService} from "@ofServices/external-devices.service"; +import {UserService} from "@ofServices/user.service"; +import {UserConfiguration} from "@ofModel/external-devices.model"; @Component({ selector: 'of-settings', @@ -31,7 +34,12 @@ export class SettingsComponent implements OnInit { replayEnabledDefaultValue: boolean; replayIntervalDefaultValue: number; - constructor(private store: Store,private configService: ConfigService) { } + userConfiguration: UserConfiguration; + + constructor(private store: Store, + private configService: ConfigService, + private userService: UserService, + private externalDevicesService: ExternalDevicesService) { } ngOnInit() { this.locales = this.configService.getConfigValue('i18n.supported.locales'); @@ -44,6 +52,17 @@ export class SettingsComponent implements OnInit { this.playSoundForInformationDefaultValue = !!this.configService.getConfigValue('settings.playSoundForInformation') ? this.configService.getConfigValue('settings.playSoundForInformation') : false; this.replayEnabledDefaultValue = !!this.configService.getConfigValue('settings.replayEnabled') ? this.configService.getConfigValue('settings.replayEnabled') : false; this.replayIntervalDefaultValue = !!this.configService.getConfigValue('settings.replayInterval') ? this.configService.getConfigValue('settings.replayInterval') : 5; + + const userLogin = this.userService.getCurrentUserWithPerimeters().userData.login; + + if (this.externalDevicesEnabled) + this.externalDevicesService.fetchUserConfiguration(userLogin).subscribe(result => { + this.userConfiguration = result; + }); + } + + isExternalDeviceConfiguredForUser() : boolean { + return (!!this.userConfiguration && !!this.userConfiguration.externalDeviceId); } } diff --git a/ui/main/src/app/modules/share/timeline-buttons/timeline-buttons.component.ts b/ui/main/src/app/modules/share/timeline-buttons/timeline-buttons.component.ts index 34b083cd63..42be866cf1 100644 --- a/ui/main/src/app/modules/share/timeline-buttons/timeline-buttons.component.ts +++ b/ui/main/src/app/modules/share/timeline-buttons/timeline-buttons.component.ts @@ -80,6 +80,7 @@ export class TimelineButtonsComponent implements OnInit { }, TR: { buttonTitle: 'timeline.buttonTitle.TR', domainId : 'TR', + followClockTick: true }, '7D': { buttonTitle: 'timeline.buttonTitle.7D', domainId:'7D', diff --git a/ui/main/src/app/services/authentication/authentication.service.ts b/ui/main/src/app/services/authentication/authentication.service.ts index ef2a295b78..77d5b7039e 100644 --- a/ui/main/src/app/services/authentication/authentication.service.ts +++ b/ui/main/src/app/services/authentication/authentication.service.ts @@ -1,5 +1,5 @@ /* Copyright (c) 2020, RTEi (http://www.rte-international.com) - * Copyright (c) 2021, RTE (http://www.rte-france.com) + * Copyright (c) 2021-2022, RTE (http://www.rte-france.com) * See AUTHORS.txt * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -20,7 +20,7 @@ import { InitAuthStatus, PayloadForSuccessfulAuthentication, RejectLogIn, - UnableToRefreshOrGetToken, + SessionExpired, UnAuthenticationFromImplicitFlow } from '@ofActions/authentication.actions'; import {environment} from '@env/environment'; @@ -145,7 +145,7 @@ export class AuthenticationService { this.regularCheckTokenValidity(); }, MILLIS_TO_WAIT_BETWEEN_TOKEN_EXPIRATION_DATE_CONTROLS); } else {// Will send Logout if token is expired - this.store.dispatch(new UnableToRefreshOrGetToken()); + this.store.dispatch(new SessionExpired()); } } @@ -528,7 +528,7 @@ export class ImplicitAuthenticationHandler implements AuthenticationModeHandler // This case arise for example when using a SSO and the session is not valid anymore (session timeout) case ('token_error'): case('token_refresh_error'): - this.store.dispatch(new UnableToRefreshOrGetToken()); + this.store.dispatch(new SessionExpired()); break; case('logout'): { this.store.dispatch(new UnAuthenticationFromImplicitFlow()); diff --git a/ui/main/src/app/services/card.service.ts b/ui/main/src/app/services/card.service.ts index a757ff98e3..132b09c172 100644 --- a/ui/main/src/app/services/card.service.ts +++ b/ui/main/src/app/services/card.service.ts @@ -22,7 +22,7 @@ import {Page} from '@ofModel/page.model'; import {AppState} from '@ofStore/index'; import {Store} from '@ngrx/store'; import {CardSubscriptionClosed, CardSubscriptionOpen} from '@ofActions/cards-subscription.actions'; -import {catchError} from 'rxjs/operators'; +import {catchError, takeUntil} from 'rxjs/operators'; import {RemoveLightCard} from '@ofActions/light-card.actions'; import {BusinessConfigChangeAction} from '@ofStore/actions/processes.actions'; import {UserConfigChangeAction} from '@ofStore/actions/user.actions'; @@ -43,9 +43,10 @@ export class CardService { readonly cardsPubUrl: string; readonly userCardReadUrl: string; readonly userCardUrl: string; - private lastHeardBeatDate: number; + private lastHeardBeatDate: number = 0; private firstSubscriptionInitDone = false; public initSubscription = new Subject(); + private unsubscribe$: Subject = new Subject(); private startOfAlreadyLoadedPeriod: number; private endOfAlreadyLoadedPeriod: number; @@ -81,6 +82,7 @@ export class CardService { public initCardSubscription() { this.getCardSubscription() + .pipe(takeUntil(this.unsubscribe$)) .subscribe( { next: operation => { switch (operation.type) { @@ -121,6 +123,9 @@ export class CardService { } + public closeSubscription() { + this.unsubscribe$.next(); + } private getCardSubscription(): Observable { // security header needed here as SSE request are not intercepted by our header interceptor @@ -145,24 +150,30 @@ export class CardService { console.log(new Date().toISOString(), `CardService - Card subscription initialized`); this.initSubscription.next(); this.initSubscription.complete(); - if (this.firstSubscriptionInitDone) this.recoverAnyLostCardWhenConnectionHasBeenReset(); - else this.firstSubscriptionInitDone = true; + if (this.firstSubscriptionInitDone) { + this.recoverAnyLostCardWhenConnectionHasBeenReset(); + // process or user config may have change during connection loss + // so reload both configuration + this.store.dispatch(new BusinessConfigChangeAction()); + this.store.dispatch(new UserConfigChangeAction()); + } + else { + this.firstSubscriptionInitDone = true; + this.lastHeardBeatDate = new Date().valueOf(); + } break; case 'HEARTBEAT': this.lastHeardBeatDate = new Date().valueOf(); console.log(new Date().toISOString(), `CardService - HEARTBEAT received - Connection alive `); break; - case 'RESTORE': - console.log(new Date().toISOString(), `CardService - Subscription restored with server`); - break; case 'BUSINESS_CONFIG_CHANGE': this.store.dispatch(new BusinessConfigChangeAction()); console.log(new Date().toISOString(), `CardService - BUSINESS_CONFIG_CHANGE received`); break; case 'USER_CONFIG_CHANGE': - this.store.dispatch(new UserConfigChangeAction()); - console.log(new Date().toISOString(), `CardService - USER_CONFIG_CHANGE received`); - break; + this.store.dispatch(new UserConfigChangeAction()); + console.log(new Date().toISOString(), `CardService - USER_CONFIG_CHANGE received`); + break; default : return observer.next(JSON.parse(message.data, CardOperation.convertTypeIntoEnum)); } diff --git a/ui/main/src/app/services/entities.service.ts b/ui/main/src/app/services/entities.service.ts index 799adb0870..0561977e72 100644 --- a/ui/main/src/app/services/entities.service.ts +++ b/ui/main/src/app/services/entities.service.ts @@ -92,6 +92,7 @@ export class EntitiesService extends CachedCrudService implements OnDestroy { if (!!entities) { this._entities = entities; this.setEntityNamesInTemplateGateway(); + this.setEntitiesInTemplateGateway(); console.log(new Date().toISOString(), 'List of entities loaded'); } }, @@ -125,6 +126,19 @@ export class EntitiesService extends CachedCrudService implements OnDestroy { templateGateway.setEntityNames(entityNames); } + private setEntitiesInTemplateGateway(): void { + const entities = new Map(); + this._entities.forEach(entity => entities.set(entity.id, + { id: entity.id, + name: entity.name, + description: entity.description, + entityAllowedToSendCard: entity.entityAllowedToSendCard, + parents: entity.parents + }) + ); + templateGateway.setEntities(entities); + } + /** Given a list of entities that might contain parent entities, this method returns the list of entities * that can actually send cards * */ diff --git a/ui/main/src/app/services/external-devices.service.ts b/ui/main/src/app/services/external-devices.service.ts index ac8fddf010..07f72e9978 100644 --- a/ui/main/src/app/services/external-devices.service.ts +++ b/ui/main/src/app/services/external-devices.service.ts @@ -11,7 +11,7 @@ import {environment} from '@env/environment'; import {HttpClient} from '@angular/common/http'; import {catchError} from 'rxjs/operators'; import {Observable, Subject} from 'rxjs'; -import {Notification} from "@ofModel/external-devices.model"; +import {Notification, UserConfiguration} from "@ofModel/external-devices.model"; import {Injectable} from '@angular/core'; import {ErrorService} from "@ofServices/error-service"; @@ -22,6 +22,7 @@ export class ExternalDevicesService extends ErrorService { readonly externalDevicesUrl: string; readonly notificationsUrl: string; + readonly configurationsUrl: string; private ngUnsubscribe$ = new Subject(); /** * @constructor @@ -31,6 +32,7 @@ export class ExternalDevicesService extends ErrorService { super(); this.externalDevicesUrl = `${environment.urls.externalDevices}`; this.notificationsUrl = this.externalDevicesUrl+'/notifications'; + this.configurationsUrl = this.externalDevicesUrl+'/configurations'; } sendNotification(notification: Notification): Observable { @@ -39,4 +41,8 @@ export class ExternalDevicesService extends ErrorService { ); } + fetchUserConfiguration(login: string): Observable { + return this.httpClient.get(`${this.configurationsUrl}/users/${login}`); + } + } diff --git a/ui/main/src/app/services/lightcards/filter.service.spec.ts b/ui/main/src/app/services/lightcards/filter.service.spec.ts index a6b711d35b..0b76b9fe4e 100644 --- a/ui/main/src/app/services/lightcards/filter.service.spec.ts +++ b/ui/main/src/app/services/lightcards/filter.service.spec.ts @@ -23,7 +23,7 @@ describe('NewFilterService ', () => { }); - function getFourCard() { + function getFourCards() { let cards: LightCard[] = new Array(); cards = cards.concat(getSeveralRandomLightCards(1, { startDate: new Date().valueOf(), @@ -63,6 +63,35 @@ describe('NewFilterService ', () => { } + function getSevenCards() { + let cards = getFourCards(); + cards = cards.concat(getSeveralRandomLightCards(1, { + startDate: new Date().valueOf() + 36 * ONE_HOUR, + endDate: new Date().valueOf() + 48 * ONE_HOUR, + publishDate : new Date().valueOf() + ONE_HOUR * 25, + severity: Severity.INFORMATION, + hasBeenAcknowledged: true, + hasChildCardFromCurrentUserEntity: false + })); + cards = cards.concat(getSeveralRandomLightCards(1, { + startDate: new Date().valueOf() + 31 * ONE_HOUR, + endDate: new Date().valueOf() + 48 * ONE_HOUR, + publishDate : new Date().valueOf() - ONE_HOUR * 31, + severity: Severity.INFORMATION, + hasBeenAcknowledged: true, + hasChildCardFromCurrentUserEntity: false + })); + cards = cards.concat(getSeveralRandomLightCards(1, { + startDate: new Date().valueOf() + 31 * ONE_HOUR, + endDate: new Date().valueOf() + 48 * ONE_HOUR, + publishDate : new Date().valueOf() + ONE_HOUR * 51, + severity: Severity.INFORMATION, + hasBeenAcknowledged: true, + hasChildCardFromCurrentUserEntity: false + })); + return cards; + } + describe('ack filter', () => { it('filter 0 cards shall return 0 cards ', () => { const cards: LightCard[] = new Array(); @@ -70,7 +99,7 @@ describe('NewFilterService ', () => { expect( filteredCards.length).toBe(0); }); it('filter 4 cards with two ack shall return 2 cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); const filteredCards = service.filterLightCards(cards); expect(filteredCards.length).toBe(2); expect( filteredCards).toContain(cards[0]); @@ -78,7 +107,7 @@ describe('NewFilterService ', () => { }); it('filter 4 cards , filter is inative => shall return the 4 cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); const filteredCards = service.filterLightCards(cards); expect(filteredCards.length).toBe(4); @@ -93,7 +122,7 @@ describe('NewFilterService ', () => { describe('response form my own entity filter', () => { it('filter 1 with child card and 3 with no child card filter is active => shall return the 3 cards with no child ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.RESPONSE_FILTER, true, false); const filteredCards = service.filterLightCards(cards); @@ -109,7 +138,7 @@ describe('NewFilterService ', () => { describe('type filter', () => { it('filter 4 cards with 4 different severity , filter is set to alarm severity only => shall return the alarm card only ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.TYPE_FILTER, true, {alarm: true, action: false, compliant: false, information: false }); const filteredCards = service.filterLightCards(cards); @@ -120,7 +149,7 @@ describe('NewFilterService ', () => { it('filter 4 cards with 4 different severity , filter is set to action/compliant/information severity => shall return 3 cards', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.TYPE_FILTER, true, {alarm: false, action: true, compliant: true, information: true }); const filteredCards = service.filterLightCards(cards); @@ -135,7 +164,7 @@ describe('NewFilterService ', () => { describe('business date filter', () => { it('Filter with start date after card 1 startDate => shoud return 3 cards ', () => { - const cards = getFourCard(); + const cards = getSevenCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.BUSINESSDATE_FILTER, true, { start: new Date().valueOf() + 0.5 * ONE_HOUR, @@ -148,40 +177,64 @@ describe('NewFilterService ', () => { expect(filteredCards).toContain(cards[3]); }); - it('Filter with business period matching card 3 & 4 => shoud return 1 cards ', () => { - const cards = getFourCard(); + it('Filter with business period matching card 3 ,4 , 5 => shoud return 3 cards ', () => { + const cards = getSevenCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.BUSINESSDATE_FILTER, true, { start: new Date().valueOf() + 1.5 * ONE_HOUR, end : new Date().valueOf() + 30 * ONE_HOUR }); const filteredCards = service.filterLightCards(cards); - expect(filteredCards.length).toBe(2); + expect(filteredCards.length).toBe(3); expect(filteredCards).toContain(cards[2]); expect(filteredCards).toContain(cards[3]); + expect(filteredCards).toContain(cards[4]); }); it('Filter with business period matching card 4 only => shoud return 1 cards ', () => { - const cards = getFourCard(); + const cards = getSevenCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.BUSINESSDATE_FILTER, true, { start: new Date().valueOf() + 2.5 * ONE_HOUR, - end : new Date().valueOf() + 30 * ONE_HOUR + end : new Date().valueOf() + 20 * ONE_HOUR }); const filteredCards = service.filterLightCards(cards); expect(filteredCards.length).toBe(1); expect(filteredCards).toContain(cards[3]); }); - it('Filter with start date after all business period => shoud return 0 cards ', () => { - const cards = getFourCard(); + it('Filter with start date after all business period, card 5 has publish date in business period => shoud return 1 cards ', () => { + const cards = getSevenCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.BUSINESSDATE_FILTER, true, { start: new Date().valueOf() + 20 * ONE_HOUR, end : new Date().valueOf() + 30 * ONE_HOUR }); const filteredCards = service.filterLightCards(cards); - expect(filteredCards.length).toBe(0); + expect(filteredCards.length).toBe(1); + expect(filteredCards).toContain(cards[4]); + }); + + it('Filter with end date before all business period, card 6 has publish date before end date => shoud return 1 cards ', () => { + const cards = getSevenCards(); + service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); + service.updateFilter(FilterType.BUSINESSDATE_FILTER, true, { + end : new Date().valueOf() - 30 * ONE_HOUR + }); + const filteredCards = service.filterLightCards(cards); + expect(filteredCards.length).toBe(1); + expect(filteredCards).toContain(cards[5]); + }); + + it('Filter with start date after all business periods, card 7 has publish date after start date => shoud return 1 cards ', () => { + const cards = getSevenCards(); + service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); + service.updateFilter(FilterType.BUSINESSDATE_FILTER, true, { + start: new Date().valueOf() + 50 * ONE_HOUR, + }); + const filteredCards = service.filterLightCards(cards); + expect(filteredCards.length).toBe(1); + expect(filteredCards).toContain(cards[6]); }); }); @@ -189,7 +242,7 @@ describe('NewFilterService ', () => { describe('publish date filter', () => { it('Filter with start date before all date => shoud return the four cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.PUBLISHDATE_FILTER, true, {start: new Date().valueOf() - 4 * ONE_HOUR} ); const filteredCards = service.filterLightCards(cards); @@ -202,7 +255,7 @@ describe('NewFilterService ', () => { it('Filter with start date before two date => shoud return two cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.PUBLISHDATE_FILTER, true, {start: new Date().valueOf() - 1.5 * ONE_HOUR} ); const filteredCards = service.filterLightCards(cards); @@ -212,7 +265,7 @@ describe('NewFilterService ', () => { }); it('Filter with start date after all date => shoud return no cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.PUBLISHDATE_FILTER, true, {start: new Date().valueOf() + ONE_HOUR} ); const filteredCards = service.filterLightCards(cards); @@ -220,7 +273,7 @@ describe('NewFilterService ', () => { }); it('Filter with end date after all date => shoud return the four cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.PUBLISHDATE_FILTER, true, {end: new Date().valueOf() + 4 * ONE_HOUR} ); const filteredCards = service.filterLightCards(cards); @@ -233,7 +286,7 @@ describe('NewFilterService ', () => { it('Filter with end date before two date => shoud return two cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.PUBLISHDATE_FILTER, true, {end: new Date().valueOf() - 1.5 * ONE_HOUR} ); const filteredCards = service.filterLightCards(cards); @@ -243,7 +296,7 @@ describe('NewFilterService ', () => { }); it('Filter with end date before all date => shoud return no cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.PUBLISHDATE_FILTER, true, {end: new Date().valueOf() - 5 * ONE_HOUR} ); const filteredCards = service.filterLightCards(cards); @@ -251,7 +304,7 @@ describe('NewFilterService ', () => { }); it('Filter with [start date ; end date ] => shoud return two cards ', () => { - const cards = getFourCard(); + const cards = getFourCards(); service.updateFilter(FilterType.ACKNOWLEDGEMENT_FILTER, false, false); service.updateFilter(FilterType.PUBLISHDATE_FILTER, true, { start: new Date().valueOf() - 2.5 * ONE_HOUR, diff --git a/ui/main/src/app/services/lightcards/filter.service.ts b/ui/main/src/app/services/lightcards/filter.service.ts index 57f5443a6e..398cb052af 100644 --- a/ui/main/src/app/services/lightcards/filter.service.ts +++ b/ui/main/src/app/services/lightcards/filter.service.ts @@ -111,16 +111,11 @@ export class FilterService { return new Filter( (card: LightCard, status) => { if (!!status.start && !!status.end) { - if (!card.endDate) { - return status.start <= card.startDate && card.startDate <= status.end; - } - return status.start <= card.startDate && card.startDate <= status.end - || status.start <= card.endDate && card.endDate <= status.end - || card.startDate <= status.start && status.end <= card.endDate; + return this.chechCardVisibilityinRange(card, status.start, status.end); } else if (!!status.start) { - return (!card.endDate && card.startDate >= status.start) || (!!card.endDate && status.start <= card.endDate); + return card.publishDate >= status.start || (!card.endDate && card.startDate >= status.start) || (!!card.endDate && status.start <= card.endDate); } else if (!!status.end) { - return card.startDate <= status.end; + return card.publishDate <= status.end || card.startDate <= status.end; } console.warn(new Date().toISOString(), 'Unexpected business date filter situation'); return false; @@ -132,6 +127,18 @@ export class FilterService { }); } + private chechCardVisibilityinRange(card: LightCard, start, end ) { + if (start <= card.publishDate && card.publishDate <= end) { + return true; + } + if (!card.endDate) { + return start <= card.startDate && card.startDate <= end; + } + return start <= card.startDate && card.startDate <= end + || start <= card.endDate && card.endDate <= end + || card.startDate <= start && end <= card.endDate; + } + private initPublishDateFilter(): Filter { return new Filter( diff --git a/ui/main/src/app/services/lightcards/lightcards-feed-filter.service.ts b/ui/main/src/app/services/lightcards/lightcards-feed-filter.service.ts index 749360f282..3ae990c844 100644 --- a/ui/main/src/app/services/lightcards/lightcards-feed-filter.service.ts +++ b/ui/main/src/app/services/lightcards/lightcards-feed-filter.service.ts @@ -9,7 +9,7 @@ import {Injectable} from '@angular/core'; -import {map} from 'rxjs/operators'; +import {debounceTime, map} from 'rxjs/operators'; import {combineLatest, Observable, Subject, } from 'rxjs'; import {LightCard} from '@ofModel/light-card.model'; import {LightCardsStoreService} from './lightcards-store.service'; @@ -65,6 +65,8 @@ export class LightCardsFeedFilterService { this.onlyBusinessFilterForTimeLine.asObservable(), ] ).pipe( + debounceTime(50), // When resetting components it can happen that we have more than one filter change + // with debounceTime, we avoid processing intermediate states map(results => { const lightCards = results[1]; const onlyBusinessFitlerForTimeLine = results[2]; diff --git a/ui/main/src/app/services/sound-notification.service.ts b/ui/main/src/app/services/sound-notification.service.ts index 6c79117d8b..eef257a264 100644 --- a/ui/main/src/app/services/sound-notification.service.ts +++ b/ui/main/src/app/services/sound-notification.service.ts @@ -1,4 +1,4 @@ -/* Copyright (c) 2018-2021, RTE (http://www.rte-france.com) +/* Copyright (c) 2018-2022, RTE (http://www.rte-france.com) * See AUTHORS.txt * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -9,23 +9,23 @@ import {Injectable, OnDestroy} from '@angular/core'; -import {PlatformLocation} from "@angular/common"; -import {LightCard, Severity} from "@ofModel/light-card.model"; -import {Notification} from "@ofModel/external-devices.model"; -import {Store} from "@ngrx/store"; -import {AppState} from "@ofStore/index"; -import {buildSettingsOrConfigSelector} from "@ofSelectors/settings.x.config.selectors"; +import {PlatformLocation} from '@angular/common'; +import {LightCard, Severity} from '@ofModel/light-card.model'; +import {Notification} from '@ofModel/external-devices.model'; +import {Store} from '@ngrx/store'; +import {AppState} from '@ofStore/index'; +import {buildSettingsOrConfigSelector} from '@ofSelectors/settings.x.config.selectors'; import {LightCardsFeedFilterService} from './lightcards/lightcards-feed-filter.service'; import {LightCardsStoreService} from './lightcards/lightcards-store.service'; -import {EMPTY, iif, merge, of, Subject, timer} from "rxjs"; import {filter, map, switchMap, takeUntil} from "rxjs/operators"; -import {ExternalDevicesService} from "@ofServices/external-devices.service"; -import {ConfigService} from "@ofServices/config.service"; +import {EMPTY, iif, merge, of, Subject, timer} from 'rxjs'; import {filter, map, switchMap, takeUntil} from 'rxjs/operators'; +import {ExternalDevicesService} from '@ofServices/external-devices.service'; +import {ConfigService} from '@ofServices/config.service'; @Injectable() -export class SoundNotificationService implements OnDestroy{ +export class SoundNotificationService implements OnDestroy { - private static RECENT_THRESHOLD: number = 4000; // in milliseconds + private static RECENT_THRESHOLD = 4000; // in milliseconds /* The subscription used by the front end to get cards to display in the feed from the backend doesn't distinguish * between old cards loaded from the database and new cards arriving through the notification broker. * In addition, the getCardOperation observable on which this sound notification is hooked will also emit events @@ -33,18 +33,20 @@ export class SoundNotificationService implements OnDestroy{ * once (and only new cards), sounds will only be played for a given card if the elapsed time since its publishDate * is below this threshold. */ - private static DEFAULT_REPLAY_INTERVAL: number = 5; // in seconds - private static SECONDS_TO_MILLISECONDS: number = 1000; + private static DEFAULT_REPLAY_INTERVAL = 5; // in seconds + private static SECONDS_TO_MILLISECONDS = 1000; private replayInterval: number; private soundConfigBySeverity: Map; private soundEnabled: Map; private playSoundOnExternalDevice: boolean; private replayEnabled: boolean; + private playSoundWhenSessionEnd = false; private readonly soundFileBasePath: string; private incomingCardOrReminder = new Subject(); + private sessionEnd = new Subject(); private clearSignal = new Subject(); private ngUnsubscribe$ = new Subject(); private lastSentCardId: string; @@ -66,26 +68,37 @@ export class SoundNotificationService implements OnDestroy{ this.soundConfigBySeverity.set(Severity.COMPLIANT, {soundFileName: 'compliant.mp3', soundEnabledSetting: 'playSoundForCompliant'}); this.soundConfigBySeverity.set(Severity.INFORMATION, {soundFileName: 'information.mp3', soundEnabledSetting: 'playSoundForInformation'}); - let baseHref = platformLocation.getBaseHrefFromDOM(); - this.soundFileBasePath = (baseHref ? baseHref : '/') + 'assets/sounds/' + const baseHref = platformLocation.getBaseHrefFromDOM(); + this.soundFileBasePath = (baseHref ? baseHref : '/') + 'assets/sounds/'; this.soundEnabled = new Map(); this.soundConfigBySeverity.forEach((soundConfig, severity) => { store.select(buildSettingsOrConfigSelector(soundConfig.soundEnabledSetting, false)).subscribe(x => { this.soundEnabled.set(severity, x); + this.setSoundForSessionEndWhenAtLeastOneSoundForASeverityIsActivated(); + }); }); - }) - - store.select(buildSettingsOrConfigSelector('playSoundOnExternalDevice',false)).subscribe(x => { this.playSoundOnExternalDevice = x;}) - store.select(buildSettingsOrConfigSelector('replayEnabled',false)).subscribe(x => { this.replayEnabled = x;}) - store.select(buildSettingsOrConfigSelector('replayInterval',SoundNotificationService.DEFAULT_REPLAY_INTERVAL)).subscribe(x => { this.replayInterval = x;}) + store.select(buildSettingsOrConfigSelector('playSoundOnExternalDevice', false)) + .subscribe(x => { this.playSoundOnExternalDevice = x; }); + store.select(buildSettingsOrConfigSelector('replayEnabled', false)) + .subscribe(x => { this.replayEnabled = x; }); + store.select(buildSettingsOrConfigSelector('replayInterval', SoundNotificationService.DEFAULT_REPLAY_INTERVAL)) + .subscribe(x => { this.replayInterval = x; }); - for (let severity of Object.values(Severity)) this.initSoundPlayingForSeverity(severity); + for (const severity of Object.values(Severity)) this.initSoundPlayingForSeverity(severity); + this.initSoundPlayingForSessionEnd(); this.listenForCardUpdate(); } + private setSoundForSessionEndWhenAtLeastOneSoundForASeverityIsActivated() { + this.playSoundWhenSessionEnd = false; + for (const soundEnabled of this.soundEnabled.values()) { + if (soundEnabled) this.playSoundWhenSessionEnd = true; + } + } + private listenForCardUpdate(){ this.lightCardsStoreService.getNewLightCards().subscribe( (card) => this.handleLoadedCard(card) @@ -103,7 +116,7 @@ export class SoundNotificationService implements OnDestroy{ } public handleRemindCard(card: LightCard ) { - if(this.lightCardsFeedFilterService.isCardVisibleInFeed(card)) this.incomingCardOrReminder.next(card); + if (this.lightCardsFeedFilterService.isCardVisibleInFeed(card)) this.incomingCardOrReminder.next(card); } public handleLoadedCard(card: LightCard) { @@ -114,33 +127,41 @@ export class SoundNotificationService implements OnDestroy{ } } + public handleSessionEnd() { + if (this.playSoundWhenSessionEnd) { + this.sessionEnd.next(null); + } + } + public lastSentCard(cardId: string) { this.lastSentCardId = cardId; } - private checkCardIsRecent (card: LightCard) : boolean { + private checkCardIsRecent (card: LightCard): boolean { return ((new Date().getTime() - card.publishDate) <= SoundNotificationService.RECENT_THRESHOLD); } private getSoundForSeverity(severity: Severity) : HTMLAudioElement { - return new Audio(this.soundFileBasePath+this.soundConfigBySeverity.get(severity).soundFileName); + return new Audio(this.soundFileBasePath + this.soundConfigBySeverity.get(severity).soundFileName); } - private playSoundForSeverity(severity : Severity) { + private playSoundForSeverityEnabled(severity: Severity) { - if(this.soundEnabled.get(severity)) { - if(this.configService.getConfigValue('externalDevicesEnabled') && this.playSoundOnExternalDevice) { - console.debug("External devices enabled. Sending notification for "+severity+"."); - let notification = new Notification(severity.toString()); - this.externalDevicesService.sendNotification(notification).subscribe(); - } else { - this.playSound(this.getSoundForSeverity(severity)); - } + if (this.soundEnabled.get(severity)) this.playSound(severity); + else console.debug('No sound was played for ' + severity + ' as sound is disabled for this severity'); + } + + private playSound(severity: Severity) { + if (this.configService.getConfigValue('externalDevicesEnabled') && this.playSoundOnExternalDevice) { + console.debug('External devices enabled. Sending notification for ' + severity + '.'); + const notification = new Notification(severity.toString()); + this.externalDevicesService.sendNotification(notification).subscribe(); } else { - console.debug("No sound was played for "+severity+" as sound is disabled for this severity"); + this.playSoundOnBrowser(this.getSoundForSeverity(severity)); } } + private initSoundPlayingForSeverity(severity: Severity) { merge( this.incomingCardOrReminder.pipe( @@ -154,28 +175,44 @@ export class SoundNotificationService implements OnDestroy{ // at the specified interval. In the case of SignalType.CLEAR, it creates an observable that completes immediately. // Because of the switchMap, any new observable cancels the previous one, so that a click or a new card/reminder // resets the replay timer. - .pipe(switchMap((x : SignalType) => { - if(x === SignalType.CLEAR) { - return EMPTY; - } else { - return iif(() => this.replayEnabled, - timer(0,this.replayInterval * SoundNotificationService.SECONDS_TO_MILLISECONDS), - of(null) - ); - } - }), - takeUntil(this.ngUnsubscribe$)) + .pipe(this.processSignal(),takeUntil(this.ngUnsubscribe$)) .subscribe((x ) => { console.log(new Date().toISOString() , ' Play sound'); - this.playSoundForSeverity(severity); + this.playSoundForSeverityEnabled(severity); }); } + private initSoundPlayingForSessionEnd() { + merge( + this.sessionEnd.pipe(map( x => SignalType.NOTIFICATION )), + this.clearSignal.pipe(map(x => SignalType.CLEAR)) + ) + .pipe(this.processSignal(),takeUntil(this.ngUnsubscribe$)) + .subscribe((x ) => { + console.log(new Date().toISOString() , ' Play sound for session end'); + this.playSound(Severity.ALARM); + }); + } + + private processSignal() { + return switchMap((x: SignalType) => { + if (x === SignalType.CLEAR) { + return EMPTY; + } else { + return iif(() => this.replayEnabled, + timer(0, this.replayInterval * SoundNotificationService.SECONDS_TO_MILLISECONDS), + of(null) + ); + } + }) + } + + /* There is no need to limit the frequency of calls to playSound because if a given sound XXXX is already * playing when XXXX.play() is called, nothing happens. * */ - private playSound(sound: HTMLAudioElement) { + private playSoundOnBrowser(sound: HTMLAudioElement) { sound.play().catch(error => { console.log(new Date().toISOString(), `Notification sound wasn't played because the user hasn't interacted with the app yet (autoplay policy).`); @@ -195,7 +232,7 @@ export class SoundNotificationService implements OnDestroy{ export class SoundConfig { soundFileName: string; - soundEnabledSetting:string; + soundEnabledSetting: string; } diff --git a/ui/main/src/app/store/actions/authentication.actions.ts b/ui/main/src/app/store/actions/authentication.actions.ts index ad4d11790f..39075adac7 100644 --- a/ui/main/src/app/store/actions/authentication.actions.ts +++ b/ui/main/src/app/store/actions/authentication.actions.ts @@ -25,7 +25,7 @@ export enum AuthenticationActionTypes { , UselessAuthAction = '[Authentication] Test purpose action' , ImplicitlyAuthenticated = '[Authentication] User is authentication using Implicit Flow' , UnAuthenticationFromImplicitFlow = '[Authentication] User is log out by implicit Flow internal management' - , UnableToRefreshOrGetToken = '[Authentication] The token can not be refresh or we cannot get a token' + , SessionExpired = '[Authentication] The token can not be refresh or is expired' } /** @@ -143,9 +143,9 @@ export class UnAuthenticationFromImplicitFlow implements Action { readonly type = AuthenticationActionTypes.UnAuthenticationFromImplicitFlow; } -export class UnableToRefreshOrGetToken implements Action { +export class SessionExpired implements Action { /* istanbul ignore next */ - readonly type = AuthenticationActionTypes.UnableToRefreshOrGetToken; + readonly type = AuthenticationActionTypes.SessionExpired; } @@ -162,4 +162,4 @@ export type AuthenticationActions = | UselessAuthAction | ImplicitlyAuthenticated | UnAuthenticationFromImplicitFlow - | UnableToRefreshOrGetToken; + | SessionExpired; diff --git a/ui/main/src/app/store/effects/authentication.effects.ts b/ui/main/src/app/store/effects/authentication.effects.ts index 385a617dd4..0d3b788835 100644 --- a/ui/main/src/app/store/effects/authentication.effects.ts +++ b/ui/main/src/app/store/effects/authentication.effects.ts @@ -1,4 +1,4 @@ -/* Copyright (c) 2018-2021, RTE (http://www.rte-france.com) +/* Copyright (c) 2018-2022, RTE (http://www.rte-france.com) * See AUTHORS.txt * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -19,8 +19,7 @@ import { AuthenticationActions, AuthenticationActionTypes, RejectLogIn, - TryToLogIn, - TryToLogOut + TryToLogIn } from '@ofActions/authentication.actions'; import {AuthenticationService} from '@ofServices/authentication/authentication.service'; import {catchError, map, switchMap, tap, withLatestFrom} from 'rxjs/operators'; @@ -41,16 +40,7 @@ import {TranslateService} from "@ngx-translate/core"; @Injectable() export class AuthenticationEffects { - /** - * @constructorCheckImplicitFlowAuthenticationStatus - * @param store - {Store} state manager - * @param actions$ - {Action} {Observable} of Action of the Application - * @param authService - service implementing the authentication business rules - * @param cardService - service handling request of cards - * @param router - router service to redirect user accordingly to the user authentication status or variation of it. - * @param translate - object to get translation - * - * istanbul ignore next */ + constructor(private store: Store, private actions$: Actions, private authService: AuthenticationService, @@ -170,7 +160,7 @@ export class AuthenticationEffects { * @typedef {Observable} * */ - + CheckAuthentication: Observable = createEffect(() => this.actions$ .pipe( @@ -235,16 +225,6 @@ export class AuthenticationEffects { )); - - UnableToRefreshToken: Observable = createEffect(() => - this.actions$.pipe( - ofType(AuthenticationActionTypes.UnableToRefreshOrGetToken), - switchMap(() => { - window.alert(this.translate.instant("login.error.disconnected")); - return of(new TryToLogOut()); - }) - )); - handleErrorOnTokenGeneration(errorResponse, category: string) { let message, key; const params = new Map(); @@ -270,13 +250,12 @@ export class AuthenticationEffects { handleRejectedLogin(errorMsg: Message): AuthenticationActions { this.authService.clearAuthenticationInformation(); return new RejectLogIn({error: errorMsg}); - } - + private resetState() { this.authService.clearAuthenticationInformation(); + this.cardService.closeSubscription(); window.location.href = this.configService.getConfigValue('security.logout-url','https://opfab.github.io'); - } } diff --git a/ui/main/src/app/store/effects/processes.effects.ts b/ui/main/src/app/store/effects/processes.effects.ts index e92af30b89..5c85822417 100644 --- a/ui/main/src/app/store/effects/processes.effects.ts +++ b/ui/main/src/app/store/effects/processes.effects.ts @@ -25,7 +25,7 @@ export class ProcessesEffects { updateBusinessConfig: Observable = createEffect(() => this.actions$ .pipe( ofType(ProcessesActionTypes.BusinessConfigChange), - debounce(() => timer(10000)), + debounce(() => timer(5000 + Math.floor(Math.random() * 5000))), // use a random part to avoid all UI to access at the same time the server map(() => { this.templateService.clearCache(); this.service.loadAllProcesses().subscribe(); diff --git a/ui/main/src/app/store/effects/user.effects.ts b/ui/main/src/app/store/effects/user.effects.ts index d7546aa826..bf54ca8230 100644 --- a/ui/main/src/app/store/effects/user.effects.ts +++ b/ui/main/src/app/store/effects/user.effects.ts @@ -73,7 +73,7 @@ export class UserEffects { updateUserConfig: Observable = createEffect(() => this.actions$ .pipe( ofType(UserActionsTypes.UserConfigChange), - debounce(() => timer(10000)), + debounce(() => timer(5000 + Math.floor(Math.random() * 5000))), // use a random part to avoid all UI to access at the same time the server map(() => { this.userService.loadUserWithPerimetersData().subscribe(); }), diff --git a/ui/main/src/assets/i18n/en.json b/ui/main/src/assets/i18n/en.json index d18184f5ca..7880a36f49 100755 --- a/ui/main/src/assets/i18n/en.json +++ b/ui/main/src/assets/i18n/en.json @@ -58,7 +58,8 @@ "connectionLost":"Connection lost", "tryToReconnect":"Try to reconnect ...", "activateSoundText":"Sound not activated, click to activate", - "activateSoundButton":"Activate sound" + "activateSoundButton":"Activate sound", + "sessionExpiredText":"Your session has expired" }, "menu": { "feed": "Card Feed", diff --git a/ui/main/src/assets/i18n/fr.json b/ui/main/src/assets/i18n/fr.json index 7c4fce8c61..43e933dc77 100755 --- a/ui/main/src/assets/i18n/fr.json +++ b/ui/main/src/assets/i18n/fr.json @@ -58,7 +58,8 @@ "connectionLost":"Connexion perdue", "tryToReconnect":"Tentative de reconnexion ...", "activateSoundText":"Son non activé, cliquer pour activer", - "activateSoundButton":"Activer le son" + "activateSoundButton":"Activer le son", + "sessionExpiredText":"Votre session a expiré" }, "menu": { "feed": "Flux de cartes", @@ -224,7 +225,7 @@ "search": "RECHERCHER" }, "feedConfiguration": { - "title": "CONFIGURATIONS DES NOTIFICATIONS", + "title": "CONFIGURATION DES NOTIFICATIONS", "confirmSettings": "ENREGISTRER LA CONFIGURATION", "popup": { "title": "CONFIRMATION", diff --git a/ui/main/src/assets/js/templateGateway.js b/ui/main/src/assets/js/templateGateway.js index c383d103b3..84a3c9b613 100644 --- a/ui/main/src/assets/js/templateGateway.js +++ b/ui/main/src/assets/js/templateGateway.js @@ -8,7 +8,8 @@ */ const templateGateway = { - opfabEntityNames : null, + opfabEntityNames : null, + opfabEntities : null, childCards: [], userAllowedToRespond : false, userMemberOfAnEntityRequiredToRespond : false, @@ -21,6 +22,10 @@ const templateGateway = { this.opfabEntityNames = entityNames; }, + setEntities: function(entities){ + this.opfabEntities = entities; + }, + // UTILITIES FOR TEMPLATE @@ -36,6 +41,22 @@ const templateGateway = { return this.opfabEntityNames.get(entityId); }, + getEntity: function (entityId) { + if (!this.opfabEntities) { + console.log(new Date().toISOString() , ` Template.js : no entities information loaded`); + return entityId; + } + if (!this.opfabEntities.has(entityId)) { + console.log(new Date().toISOString() , ` Template.js : entityId ${entityId} is unknown`); + return entityId; + } + return this.opfabEntities.get(entityId); + }, + + getAllEntities: function() { + return Array.from(this.opfabEntities.values()); + }, + redirectToBusinessMenu: function(menuId,menuItemId,params){ const urlSplit = document.location.href.split('#'); var newUrl = urlSplit[0] + '#/businessconfigparty/' + menuId + '/' + menuItemId ; diff --git a/ui/main/src/karma.conf.js b/ui/main/src/karma.conf.js index 80ada13858..081bde5f1b 100755 --- a/ui/main/src/karma.conf.js +++ b/ui/main/src/karma.conf.js @@ -9,7 +9,6 @@ module.exports = function (config) { require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), require('karma-mocha-reporter'), require('@angular-devkit/build-angular/plugins/karma'), require('karma-junit-reporter') @@ -17,11 +16,6 @@ module.exports = function (config) { client: { clearContext: false // leave Jasmine Spec Runner output visible in browser }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../reports/coverage'), - reports: ['html', 'lcovonly'], - fixWebpackSourcePaths: true - }, junitReporter: { outputDir: '../reports/test', // results will be saved as $outputDir/$browserName.xml outputFile: 'junit.xml', // if included, results will be saved as $outputDir/$browserName/$outputFile @@ -29,7 +23,7 @@ module.exports = function (config) { }, // add converage-istanbul for migration to angular 13 , see // https://stackoverflow.com/questions/70045859/after-upgrading-to-angular-13-the-tests-with-code-coverage-is-failing/70046050 - reporters: ['mocha', 'kjhtml', 'junit','coverage-istanbul'], + reporters: ['mocha', 'kjhtml', 'junit'], port: 9876, colors: true, logLevel: config.LOG_INFO, diff --git a/ui/main/src/scss/styles.scss b/ui/main/src/scss/styles.scss index f051d333c7..23c9c9d048 100644 --- a/ui/main/src/scss/styles.scss +++ b/ui/main/src/scss/styles.scss @@ -85,7 +85,7 @@ body { animation: fa-spin 2s infinite linear; } -// Spinner when loading cards or table of cards +// Spinner when loading cards or table of cards // the spinner is in the center of the screen .opfab-card-loading-spinner { @@ -939,5 +939,13 @@ html, body { height: 100%; } padding-right: 0px;} .opfab-sev-information {background-color: $sev-information;font-size: 0px; padding-right: 0px; } + + .ag-cell-wrap-text { + word-break: normal; + } +} + +.ag-popup { + @extend .opfab-ag-grid-theme; } diff --git a/web-ui/src/main/docker/Dockerfile b/web-ui/src/main/docker/Dockerfile index 94741a8499..9e33136527 100755 --- a/web-ui/src/main/docker/Dockerfile +++ b/web-ui/src/main/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:1.21.4-alpine +FROM nginx:1.21.5-alpine VOLUME /tmp ARG http_proxy ARG https_proxy diff --git a/web-ui/web-ui.gradle b/web-ui/web-ui.gradle index efd0207a5f..aa6f3e25b5 100755 --- a/web-ui/web-ui.gradle +++ b/web-ui/web-ui.gradle @@ -4,12 +4,9 @@ plugins { } docker { - - if (project.version.toUpperCase().equals("SNAPSHOT")) - name "lfeoperatorfabric/of-${project.name.toLowerCase()}:${project.version.toUpperCase()}" /* more information : https://vsupalov.com/docker-latest-tag/ */ - else - name "lfeoperatorfabric/of-${project.name.toLowerCase()}" - tags "latest", "${project.version}" + name "lfeoperatorfabric/of-${project.name.toLowerCase()}:${project.version}" + if (!project.version.equals("SNAPSHOT")) + tag "latest", "latest" labels(['project': "${project.group}"]) copySpec.with { from('../ui/main/build/distribution') {