diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/pom.xml b/hawkbit-dmf/hawkbit-dmf-amqp/pom.xml index 79db813ae..731045952 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/pom.xml +++ b/hawkbit-dmf/hawkbit-dmf-amqp/pom.xml @@ -10,88 +10,88 @@ --> - - 4.0.0 - - org.eclipse.hawkbit - hawkbit-dmf-parent - ${revision} - - hawkbit-dmf-amqp - hawkBit :: DMF :: AMQP 0.9 Implementation + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-dmf-parent + ${revision} + + hawkbit-dmf-amqp + hawkBit :: DMF :: AMQP 0.9 Implementation - - - org.eclipse.hawkbit - hawkbit-repository-api - ${project.version} - - - org.eclipse.hawkbit - hawkbit-core - ${project.version} - - - org.eclipse.hawkbit - hawkbit-security-integration - ${project.version} - - - org.eclipse.hawkbit - hawkbit-dmf-api - ${project.version} - - - org.springframework.boot - spring-boot-autoconfigure - - - org.springframework.amqp - spring-rabbit - - - org.springframework.security - spring-security-web - - - org.springframework.boot - spring-boot - - - org.springframework.boot - spring-boot-starter-json - - - org.slf4j - slf4j-api - - - org.springframework.boot - spring-boot-starter-logging - - - org.apache.commons - commons-collections4 - - + + + org.eclipse.hawkbit + hawkbit-repository-api + ${project.version} + + + org.eclipse.hawkbit + hawkbit-core + ${project.version} + + + org.eclipse.hawkbit + hawkbit-security-integration + ${project.version} + + + org.eclipse.hawkbit + hawkbit-dmf-api + ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.amqp + spring-rabbit + + + org.springframework.security + spring-security-web + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-starter-json + + + org.slf4j + slf4j-api + + + org.springframework.boot + spring-boot-starter-logging + + + org.apache.commons + commons-collections4 + - - - org.eclipse.hawkbit - hawkbit-dmf-rabbitmq-test - ${project.version} - test - - - io.qameta.allure - allure-junit5 - test - - - org.springframework.amqp - spring-rabbit-test - test - - + + + + org.eclipse.hawkbit + hawkbit-dmf-rabbitmq-test + ${project.version} + test + + + io.qameta.allure + allure-junit5 + test + + + org.springframework.amqp + spring-rabbit-test + test + + \ No newline at end of file diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AbstractAmqpErrorHandler.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AbstractAmqpErrorHandler.java index da4b3e5db..9cfabcc98 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AbstractAmqpErrorHandler.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AbstractAmqpErrorHandler.java @@ -31,19 +31,17 @@ public abstract class AbstractAmqpErrorHandler implements AmqpErrorHandler { /** * Returns the class of the exception. * - * @return - * the exception class + * @return the exception class */ public abstract Class getExceptionClass(); /** * Returns the customized error message. * - * @return - * the customized error message + * @return the customized error message */ - public String getErrorMessage(Throwable throwable){ - return AmqpErrorMessageComposer.constructErrorMessage(throwable); + public String getErrorMessage(Throwable throwable) { + return AmqpErrorMessageComposer.constructErrorMessage(throwable); } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java index cbec6aae9..1898ea543 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java @@ -9,6 +9,11 @@ */ package org.eclipse.hawkbit.amqp; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import lombok.extern.slf4j.Slf4j; import org.eclipse.hawkbit.api.ArtifactUrlHandler; import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings; @@ -46,11 +51,6 @@ import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.ErrorHandler; -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Spring configuration for AMQP based DMF communication for indirect device * integration. @@ -76,9 +76,7 @@ public class AmqpConfiguration { /** * Creates a custom error handler bean. * - * @param handlers - * list of {@link AmqpErrorHandler} handlers - + * @param handlers list of {@link AmqpErrorHandler} handlers * @return the delegating error handler bean */ @Bean @@ -253,15 +251,10 @@ public class AmqpConfiguration { /** * Create AMQP handler service bean. * - * @param rabbitTemplate - * for converting messages - * @param amqpMessageDispatcherService - * to sending events to DMF client - * @param controllerManagement - * for target repo access - * @param entityFactory - * to create entities - * + * @param rabbitTemplate for converting messages + * @param amqpMessageDispatcherService to sending events to DMF client + * @param controllerManagement for target repo access + * @param entityFactory to create entities * @return handler service bean */ @Bean diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpDeadletterProperties.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpDeadletterProperties.java index 35e55a8ca..34aab310a 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpDeadletterProperties.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpDeadletterProperties.java @@ -35,9 +35,8 @@ public class AmqpDeadletterProperties { /** * Return the deadletter arguments. - * - * @param exchange - * the deadletter exchange + * + * @param exchange the deadletter exchange * @return map which holds the properties */ public Map getDeadLetterExchangeArgs(final String exchange) { @@ -48,7 +47,7 @@ public class AmqpDeadletterProperties { /** * Create a deadletter queue with ttl for messages - * + * * @param queueName the deadletter queue name * @return the deadletter queue */ diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandler.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandler.java index f6be5d3b4..9fb879530 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandler.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandler.java @@ -19,10 +19,8 @@ public interface AmqpErrorHandler { /** * Handles the error based on the type of exception * - * @param throwable - * the throwable - * @param chain - * an {@link AmqpErrorHandlerChain} + * @param throwable the throwable + * @param chain an {@link AmqpErrorHandlerChain} */ void doHandle(final Throwable throwable, final AmqpErrorHandlerChain chain); } \ No newline at end of file diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandlerChain.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandlerChain.java index b2d08aaa5..58985a075 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandlerChain.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorHandlerChain.java @@ -9,27 +9,26 @@ */ package org.eclipse.hawkbit.amqp; -import org.springframework.util.ErrorHandler; - import java.util.Iterator; import java.util.List; +import org.springframework.util.ErrorHandler; + /** * An error handler chain that delegates the error to the matching error handler based on the type of exception */ public final class AmqpErrorHandlerChain { + private final Iterator iterator; private final ErrorHandler defaultHandler; /** * Constructor. * - * @param iterator - * the {@link AmqpErrorHandler} iterator - * @param defaultHandler - * the default handler + * @param iterator the {@link AmqpErrorHandler} iterator + * @param defaultHandler the default handler */ - private AmqpErrorHandlerChain(Iterator iterator, ErrorHandler defaultHandler) { + private AmqpErrorHandlerChain(Iterator iterator, ErrorHandler defaultHandler) { this.iterator = iterator; this.defaultHandler = defaultHandler; } @@ -37,10 +36,8 @@ public final class AmqpErrorHandlerChain { /** * Returns an {@link AmqpErrorHandlerChain} * - * @param errorHandlers - * {@link List} of error handlers - * @param defaultHandler - * the default error handler + * @param errorHandlers {@link List} of error handlers + * @param defaultHandler the default error handler * @return an {@link AmqpErrorHandlerChain} */ public static AmqpErrorHandlerChain getHandlerChain(final List errorHandlers, final ErrorHandler defaultHandler) { @@ -50,8 +47,7 @@ public final class AmqpErrorHandlerChain { /** * Handles the error based on the type of exception * - * @param error - * the throwable containing the cause of exception + * @param error the throwable containing the cause of exception */ public void handle(final Throwable error) { if (iterator.hasNext()) { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorMessageComposer.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorMessageComposer.java index 79635e6c3..8d6c18de4 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorMessageComposer.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpErrorMessageComposer.java @@ -27,10 +27,8 @@ public final class AmqpErrorMessageComposer { /** * Constructs an error message based on failed message content * - * @param throwable - * the throwable containing failed message content - * @return - * meaningful error message + * @param throwable the throwable containing failed message content + * @return meaningful error message */ public static String constructErrorMessage(final Throwable throwable) { StringBuilder completeErrorMessage = new StringBuilder(); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java index 6a0d71aa8..f7d59deeb 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java @@ -106,26 +106,16 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { /** * Constructor. * - * @param rabbitTemplate - * the rabbitTemplate - * @param amqpSenderService - * to send AMQP message - * @param artifactUrlHandler - * for generating download URLs - * @param systemSecurityContext - * for execution with system permissions - * @param systemManagement - * the systemManagement - * @param targetManagement - * to access target information - * @param serviceMatcher - * to check in cluster case if the message is from the same - * cluster node - * @param distributionSetManagement - * to retrieve modules - * @param tenantConfigurationManagement - * to access tenant configuration - * + * @param rabbitTemplate the rabbitTemplate + * @param amqpSenderService to send AMQP message + * @param artifactUrlHandler for generating download URLs + * @param systemSecurityContext for execution with system permissions + * @param systemManagement the systemManagement + * @param targetManagement to access target information + * @param serviceMatcher to check in cluster case if the message is from the same + * cluster node + * @param distributionSetManagement to retrieve modules + * @param tenantConfigurationManagement to access tenant configuration */ protected AmqpMessageDispatcherService(final RabbitTemplate rabbitTemplate, final AmqpMessageSenderService amqpSenderService, final ArtifactUrlHandler artifactUrlHandler, @@ -147,12 +137,16 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { this.tenantConfigurationManagement = tenantConfigurationManagement; } + public boolean isBatchAssignmentsEnabled() { + return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement + .getConfigurationValue(BATCH_ASSIGNMENTS_ENABLED, Boolean.class).getValue()); + } + /** * Method to send a message to a RabbitMQ Exchange after the Distribution * set has been assign to a Target. * - * @param assignedEvent - * the object to be send. + * @param assignedEvent the object to be send. */ @EventListener(classes = TargetAssignDistributionSetEvent.class) protected void targetAssignDistributionSet(final TargetAssignDistributionSetEvent assignedEvent) { @@ -173,8 +167,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { /** * Listener for Multi-Action events. * - * @param multiActionEvent - * the Multi-Action event to be processed + * @param multiActionEvent the Multi-Action event to be processed */ @EventListener(classes = MultiActionEvent.class) protected void onMultiAction(final MultiActionEvent multiActionEvent) { @@ -185,6 +178,261 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { sendMultiActionRequestMessages(multiActionEvent.getTenant(), multiActionEvent.getControllerIds()); } + protected void sendUpdateMessageToTarget(final ActionProperties actionsProps, final Target target, + final Map> softwareModules) { + final Map actionProp = new HashMap<>(); + actionProp.put(target.getControllerId(), actionsProps); + sendUpdateMessageToTargets(actionProp, Collections.singletonList(target), softwareModules); + } + + protected void sendMultiActionRequestToTarget(final String tenant, final Target target, final List actions, + final Function>> getSoftwareModuleMetaData) { + + final URI targetAddress = target.getAddress(); + if (!IpUtil.isAmqpUri(targetAddress) || CollectionUtils.isEmpty(actions)) { + return; + } + + final DmfMultiActionRequest multiActionRequest = new DmfMultiActionRequest(); + actions.forEach(action -> { + final DmfActionRequest actionRequest = createDmfActionRequest(target, action, + getSoftwareModuleMetaData.apply(action)); + final int weight = deploymentManagement.getWeightConsideringDefault(action); + multiActionRequest.addElement(getEventTypeForAction(action), actionRequest, weight); + }); + + final Message message = getMessageConverter().toMessage(multiActionRequest, + createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), EventTopic.MULTI_ACTION)); + amqpSenderService.sendMessage(message, targetAddress); + } + + protected DmfDownloadAndUpdateRequest createDownloadAndUpdateRequest(final Target target, final Long actionId, + final Map> softwareModules) { + final DmfDownloadAndUpdateRequest request = new DmfDownloadAndUpdateRequest(); + request.setActionId(actionId); + request.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); + + if (softwareModules != null) { + softwareModules.entrySet() + .forEach(entry -> request.addSoftwareModule(convertToAmqpSoftwareModule(target, entry))); + } + return request; + } + + /** + * Method to send a message to a RabbitMQ Exchange after the assignment of + * the Distribution set to a Target has been canceled. + * + * @param cancelEvent that is to be converted to a DMF message + */ + @EventListener(classes = CancelTargetAssignmentEvent.class) + protected void targetCancelAssignmentToDistributionSet(final CancelTargetAssignmentEvent cancelEvent) { + if (!shouldBeProcessed(cancelEvent)) { + return; + } + + final List eventTargets = partitionedParallelExecution(cancelEvent.getActions().keySet(), + targetManagement::getByControllerID); + + eventTargets.forEach(target -> { + cancelEvent.getActionPropertiesForController(target.getControllerId()).map(ActionProperties::getId) + .ifPresent(actionId -> { + sendCancelMessageToTarget(cancelEvent.getTenant(), target.getControllerId(), actionId, + target.getAddress()); + }); + }); + } + + /** + * Method to send a message to a RabbitMQ Exchange after a Target was + * deleted. + * + * @param deleteEvent the TargetDeletedEvent which holds the necessary data for + * sending a target delete message. + */ + @EventListener(classes = TargetDeletedEvent.class) + protected void targetDelete(final TargetDeletedEvent deleteEvent) { + if (!shouldBeProcessed(deleteEvent)) { + return; + } + sendDeleteMessage(deleteEvent.getTenant(), deleteEvent.getControllerId(), deleteEvent.getTargetAddress()); + } + + @EventListener(classes = TargetAttributesRequestedEvent.class) + protected void targetTriggerUpdateAttributes(final TargetAttributesRequestedEvent updateAttributesEvent) { + sendUpdateAttributesMessageToTarget(updateAttributesEvent.getTenant(), updateAttributesEvent.getControllerId(), + updateAttributesEvent.getTargetAddress()); + } + + protected void sendPingReponseToDmfReceiver(final Message ping, final String tenant, final String virtualHost) { + final Message message = MessageBuilder.withBody(String.valueOf(System.currentTimeMillis()).getBytes()) + .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) + .setCorrelationId(ping.getMessageProperties().getCorrelationId()) + .setHeader(MessageHeaderKey.TYPE, MessageType.PING_RESPONSE).setHeader(MessageHeaderKey.TENANT, tenant) + .build(); + + amqpSenderService.sendMessage(message, + IpUtil.createAmqpUri(virtualHost, ping.getMessageProperties().getReplyTo())); + } + + protected boolean shouldBeProcessed(final RemoteApplicationEvent event) { + return isFromSelf(event); + } + + protected void sendCancelMessageToTarget(final String tenant, final String controllerId, final Long actionId, + final URI address) { + if (!IpUtil.isAmqpUri(address)) { + return; + } + + final DmfActionRequest actionRequest = new DmfActionRequest(); + actionRequest.setActionId(actionId); + + final Message message = getMessageConverter().toMessage(actionRequest, + createConnectorMessagePropertiesEvent(tenant, controllerId, EventTopic.CANCEL_DOWNLOAD)); + + amqpSenderService.sendMessage(message, address); + + } + + protected DmfTarget convertToDmfTarget(final Target target, final Long actionId) { + final DmfTarget dmfTarget = new DmfTarget(); + dmfTarget.setActionId(actionId); + dmfTarget.setControllerId(target.getControllerId()); + dmfTarget.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); + return dmfTarget; + } + + /** + * Creates a Confirmation request. + * + * @param target the target + * @param actionId the actionId + * @param softwareModules the software modules + * @return + */ + protected DmfConfirmRequest createConfirmRequest(final Target target, final Long actionId, final Map> softwareModules) { + final DmfConfirmRequest request = new DmfConfirmRequest(); + request.setActionId(actionId); + request.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); + + //Software modules can be filtered in the future exposing only the needed. + if (softwareModules != null) { + softwareModules.entrySet() + .forEach(entry -> request.addSoftwareModule(convertToAmqpSoftwareModule(target, entry))); + } + return request; + } + + private static DmfActionRequest createPlainActionRequest(final Action action) { + final DmfActionRequest actionRequest = new DmfActionRequest(); + actionRequest.setActionId(action.getId()); + return actionRequest; + } + + /** + * Method to get the type of event depending on whether the action is a + * DOWNLOAD_ONLY action or if it has a valid maintenance window available or + * not based on defined maintenance schedule. In case of no maintenance + * schedule or if there is a valid window available, the topic + * {@link EventTopic#DOWNLOAD_AND_INSTALL} is returned else + * {@link EventTopic#DOWNLOAD} is returned. + * + * @param action current action properties. + * @return {@link EventTopic} to use for message. + */ + private static EventTopic getEventTypeForTarget(final ActionProperties action) { + if (action.isWaitingConfirmation()) { + return EventTopic.CONFIRM; + } + return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable()) + ? EventTopic.DOWNLOAD + : EventTopic.DOWNLOAD_AND_INSTALL; + } + + /** + * Determines the {@link EventTopic} for the given {@link Action}, depending + * on its action type. + * + * @param action to obtain the corresponding {@link EventTopic} for + * @return the {@link EventTopic} for this action + */ + private static EventTopic getEventTypeForAction(final Action action) { + if (action.isCancelingOrCanceled()) { + return EventTopic.CANCEL_DOWNLOAD; + } + return getEventTypeForTarget(new ActionProperties(action)); + } + + private static List partitionedParallelExecution(final Collection controllerIds, + final Function, List> loadingFunction) { + // Ensure not exceeding the max value of MAX_PROCESSING_SIZE + if (controllerIds.size() > MAX_PROCESSING_SIZE) { + // Split the provided collection + final Iterable> partitions = ListUtils.partition(IterableUtils.toList(controllerIds), MAX_PROCESSING_SIZE); + // Preserve the security context because it gets lost when executing + // loading calls in new threads + final SecurityContext context = SecurityContextHolder.getContext(); + // Handling remote request in parallel streams + return StreamSupport.stream(partitions.spliterator(), true) // + .flatMap(partition -> withSecurityContext(() -> loadingFunction.apply(partition), context).stream()) + .collect(Collectors.toList()); + } + return loadingFunction.apply(controllerIds); + } + + private static T withSecurityContext(final Supplier callable, final SecurityContext securityContext) { + final SecurityContext oldContext = SecurityContextHolder.getContext(); + try { + SecurityContextHolder.setContext(securityContext); + return callable.get(); + } finally { + SecurityContextHolder.setContext(oldContext); + } + } + + private static MessageProperties createConnectorMessagePropertiesEvent(final String tenant, + final String controllerId, final EventTopic topic) { + final MessageProperties messageProperties = createConnectorMessageProperties(tenant, controllerId); + messageProperties.setHeader(MessageHeaderKey.TOPIC, topic); + messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.EVENT); + return messageProperties; + } + + private static MessageProperties createConnectorMessagePropertiesDeleteThing(final String tenant, + final String controllerId) { + final MessageProperties messageProperties = createConnectorMessageProperties(tenant, controllerId); + messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.THING_DELETED); + return messageProperties; + } + + private static MessageProperties createConnectorMessageProperties(final String tenant, final String controllerId) { + final MessageProperties messageProperties = new MessageProperties(); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setHeader(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setHeader(MessageHeaderKey.THING_ID, controllerId); + messageProperties.setHeader(MessageHeaderKey.TENANT, tenant); + return messageProperties; + } + + private static MessageProperties createMessagePropertiesBatch(final String tenant, final EventTopic topic) { + final MessageProperties messageProperties = new MessageProperties(); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setHeader(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setHeader(MessageHeaderKey.TENANT, tenant); + + messageProperties.setHeader(MessageHeaderKey.TOPIC, topic); + messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.EVENT); + return messageProperties; + } + + private static EventTopic getBatchEventTopicForAction(final ActionProperties action) { + return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable()) + ? EventTopic.BATCH_DOWNLOAD + : EventTopic.BATCH_DOWNLOAD_AND_INSTALL; + } + private List getTargetsWithoutPendingCancellations(final Set controllerIds) { return partitionedParallelExecution(controllerIds, partition -> { return targetManagement.getByControllerID(partition).stream().filter(target -> { @@ -207,13 +455,6 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { }); } - protected void sendUpdateMessageToTarget(final ActionProperties actionsProps, final Target target, - final Map> softwareModules) { - final Map actionProp = new HashMap<>(); - actionProp.put(target.getControllerId(), actionsProps); - sendUpdateMessageToTargets(actionProp, Collections.singletonList(target), softwareModules); - } - private void sendUpdateMessageToTargets(final Map actionsPropsByTargetId, final List targets, final Map> softwareModules) { @@ -248,27 +489,6 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { } - protected void sendMultiActionRequestToTarget(final String tenant, final Target target, final List actions, - final Function>> getSoftwareModuleMetaData) { - - final URI targetAddress = target.getAddress(); - if (!IpUtil.isAmqpUri(targetAddress) || CollectionUtils.isEmpty(actions)) { - return; - } - - final DmfMultiActionRequest multiActionRequest = new DmfMultiActionRequest(); - actions.forEach(action -> { - final DmfActionRequest actionRequest = createDmfActionRequest(target, action, - getSoftwareModuleMetaData.apply(action)); - final int weight = deploymentManagement.getWeightConsideringDefault(action); - multiActionRequest.addElement(getEventTypeForAction(action), actionRequest, weight); - }); - - final Message message = getMessageConverter().toMessage(multiActionRequest, - createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), EventTopic.MULTI_ACTION)); - amqpSenderService.sendMessage(message, targetAddress); - } - private DmfActionRequest createDmfActionRequest(final Target target, final Action action, final Map> softwareModules) { if (action.isCancelingOrCanceled()) { @@ -279,137 +499,6 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { return createDownloadAndUpdateRequest(target, action.getId(), softwareModules); } - private static DmfActionRequest createPlainActionRequest(final Action action) { - final DmfActionRequest actionRequest = new DmfActionRequest(); - actionRequest.setActionId(action.getId()); - return actionRequest; - } - - protected DmfDownloadAndUpdateRequest createDownloadAndUpdateRequest(final Target target, final Long actionId, - final Map> softwareModules) { - final DmfDownloadAndUpdateRequest request = new DmfDownloadAndUpdateRequest(); - request.setActionId(actionId); - request.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); - - if (softwareModules != null) { - softwareModules.entrySet() - .forEach(entry -> request.addSoftwareModule(convertToAmqpSoftwareModule(target, entry))); - } - return request; - } - - /** - * Method to get the type of event depending on whether the action is a - * DOWNLOAD_ONLY action or if it has a valid maintenance window available or - * not based on defined maintenance schedule. In case of no maintenance - * schedule or if there is a valid window available, the topic - * {@link EventTopic#DOWNLOAD_AND_INSTALL} is returned else - * {@link EventTopic#DOWNLOAD} is returned. - * - * @param action - * current action properties. - * - * @return {@link EventTopic} to use for message. - */ - private static EventTopic getEventTypeForTarget(final ActionProperties action) { - if (action.isWaitingConfirmation()) { - return EventTopic.CONFIRM; - } - return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable()) - ? EventTopic.DOWNLOAD - : EventTopic.DOWNLOAD_AND_INSTALL; - } - - /** - * Determines the {@link EventTopic} for the given {@link Action}, depending - * on its action type. - * - * @param action - * to obtain the corresponding {@link EventTopic} for - * - * @return the {@link EventTopic} for this action - */ - private static EventTopic getEventTypeForAction(final Action action) { - if (action.isCancelingOrCanceled()) { - return EventTopic.CANCEL_DOWNLOAD; - } - return getEventTypeForTarget(new ActionProperties(action)); - } - - /** - * Method to send a message to a RabbitMQ Exchange after the assignment of - * the Distribution set to a Target has been canceled. - * - * @param cancelEvent - * that is to be converted to a DMF message - */ - @EventListener(classes = CancelTargetAssignmentEvent.class) - protected void targetCancelAssignmentToDistributionSet(final CancelTargetAssignmentEvent cancelEvent) { - if (!shouldBeProcessed(cancelEvent)) { - return; - } - - final List eventTargets = partitionedParallelExecution(cancelEvent.getActions().keySet(), - targetManagement::getByControllerID); - - eventTargets.forEach(target -> { - cancelEvent.getActionPropertiesForController(target.getControllerId()).map(ActionProperties::getId) - .ifPresent(actionId -> { - sendCancelMessageToTarget(cancelEvent.getTenant(), target.getControllerId(), actionId, - target.getAddress()); - }); - }); - } - - private static List partitionedParallelExecution(final Collection controllerIds, - final Function, List> loadingFunction) { - // Ensure not exceeding the max value of MAX_PROCESSING_SIZE - if (controllerIds.size() > MAX_PROCESSING_SIZE) { - // Split the provided collection - final Iterable> partitions = ListUtils.partition(IterableUtils.toList(controllerIds), MAX_PROCESSING_SIZE); - // Preserve the security context because it gets lost when executing - // loading calls in new threads - final SecurityContext context = SecurityContextHolder.getContext(); - // Handling remote request in parallel streams - return StreamSupport.stream(partitions.spliterator(), true) // - .flatMap(partition -> withSecurityContext(() -> loadingFunction.apply(partition), context).stream()) - .collect(Collectors.toList()); - } - return loadingFunction.apply(controllerIds); - } - - private static T withSecurityContext(final Supplier callable, final SecurityContext securityContext) { - final SecurityContext oldContext = SecurityContextHolder.getContext(); - try { - SecurityContextHolder.setContext(securityContext); - return callable.get(); - } finally { - SecurityContextHolder.setContext(oldContext); - } - } - - /** - * Method to send a message to a RabbitMQ Exchange after a Target was - * deleted. - * - * @param deleteEvent - * the TargetDeletedEvent which holds the necessary data for - * sending a target delete message. - */ - @EventListener(classes = TargetDeletedEvent.class) - protected void targetDelete(final TargetDeletedEvent deleteEvent) { - if (!shouldBeProcessed(deleteEvent)) { - return; - } - sendDeleteMessage(deleteEvent.getTenant(), deleteEvent.getControllerId(), deleteEvent.getTargetAddress()); - } - - @EventListener(classes = TargetAttributesRequestedEvent.class) - protected void targetTriggerUpdateAttributes(final TargetAttributesRequestedEvent updateAttributesEvent) { - sendUpdateAttributesMessageToTarget(updateAttributesEvent.getTenant(), updateAttributesEvent.getControllerId(), - updateAttributesEvent.getTargetAddress()); - } - private void sendSingleUpdateMessage(final ActionProperties action, final Target target, final Map> modules) { @@ -434,17 +523,6 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { amqpSenderService.sendMessage(message, targetAddress); } - protected void sendPingReponseToDmfReceiver(final Message ping, final String tenant, final String virtualHost) { - final Message message = MessageBuilder.withBody(String.valueOf(System.currentTimeMillis()).getBytes()) - .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) - .setCorrelationId(ping.getMessageProperties().getCorrelationId()) - .setHeader(MessageHeaderKey.TYPE, MessageType.PING_RESPONSE).setHeader(MessageHeaderKey.TENANT, tenant) - .build(); - - amqpSenderService.sendMessage(message, - IpUtil.createAmqpUri(virtualHost, ping.getMessageProperties().getReplyTo())); - } - private void sendDeleteMessage(final String tenant, final String controllerId, final String targetAddress) { if (!hasValidAddress(targetAddress)) { return; @@ -459,10 +537,6 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { return targetAddress != null && IpUtil.isAmqpUri(URI.create(targetAddress)); } - protected boolean shouldBeProcessed(final RemoteApplicationEvent event) { - return isFromSelf(event); - } - private boolean isFromSelf(final RemoteApplicationEvent event) { return serviceMatcher == null || serviceMatcher.isFromSelf(event); } @@ -471,22 +545,6 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { return deploymentManagement.hasPendingCancellations(targetId); } - protected void sendCancelMessageToTarget(final String tenant, final String controllerId, final Long actionId, - final URI address) { - if (!IpUtil.isAmqpUri(address)) { - return; - } - - final DmfActionRequest actionRequest = new DmfActionRequest(); - actionRequest.setActionId(actionId); - - final Message message = getMessageConverter().toMessage(actionRequest, - createConnectorMessagePropertiesEvent(tenant, controllerId, EventTopic.CANCEL_DOWNLOAD)); - - amqpSenderService.sendMessage(message, address); - - } - private void sendUpdateAttributesMessageToTarget(final String tenant, final String controllerId, final String targetAddress) { if (!hasValidAddress(targetAddress)) { @@ -499,30 +557,6 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { amqpSenderService.sendMessage(message, URI.create(targetAddress)); } - private static MessageProperties createConnectorMessagePropertiesEvent(final String tenant, - final String controllerId, final EventTopic topic) { - final MessageProperties messageProperties = createConnectorMessageProperties(tenant, controllerId); - messageProperties.setHeader(MessageHeaderKey.TOPIC, topic); - messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.EVENT); - return messageProperties; - } - - private static MessageProperties createConnectorMessagePropertiesDeleteThing(final String tenant, - final String controllerId) { - final MessageProperties messageProperties = createConnectorMessageProperties(tenant, controllerId); - messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.THING_DELETED); - return messageProperties; - } - - private static MessageProperties createConnectorMessageProperties(final String tenant, final String controllerId) { - final MessageProperties messageProperties = new MessageProperties(); - messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); - messageProperties.setHeader(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); - messageProperties.setHeader(MessageHeaderKey.THING_ID, controllerId); - messageProperties.setHeader(MessageHeaderKey.TENANT, tenant); - return messageProperties; - } - private DmfSoftwareModule convertToAmqpSoftwareModule(final Target target, final Entry> entry) { final DmfSoftwareModule amqpSoftwareModule = new DmfSoftwareModule(); @@ -558,9 +592,9 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { final TenantMetaData metaData = systemManagement.getTenantMetadata(); artifact.setUrls(artifactUrlHandler .getUrls(new URLPlaceholder(metaData.getTenant(), - metaData.getId(), target.getControllerId(), target.getId(), - new SoftwareData(localArtifact.getSoftwareModule().getId(), localArtifact.getFilename(), - localArtifact.getId(), localArtifact.getSha1Hash())), + metaData.getId(), target.getControllerId(), target.getId(), + new SoftwareData(localArtifact.getSoftwareModule().getId(), localArtifact.getFilename(), + localArtifact.getId(), localArtifact.getSha1Hash())), ApiType.DMF) .stream().collect(Collectors.toMap(ArtifactUrl::getProtocol, ArtifactUrl::getRef))); @@ -608,55 +642,4 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { createMessagePropertiesBatch(firstAction.getTenant(), getBatchEventTopicForAction(firstAction))); amqpSenderService.sendMessage(message, firstTarget.getAddress()); } - - protected DmfTarget convertToDmfTarget(final Target target, final Long actionId) { - final DmfTarget dmfTarget = new DmfTarget(); - dmfTarget.setActionId(actionId); - dmfTarget.setControllerId(target.getControllerId()); - dmfTarget.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); - return dmfTarget; - } - - private static MessageProperties createMessagePropertiesBatch(final String tenant, final EventTopic topic) { - final MessageProperties messageProperties = new MessageProperties(); - messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); - messageProperties.setHeader(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); - messageProperties.setHeader(MessageHeaderKey.TENANT, tenant); - - messageProperties.setHeader(MessageHeaderKey.TOPIC, topic); - messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.EVENT); - return messageProperties; - } - - public boolean isBatchAssignmentsEnabled() { - return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(BATCH_ASSIGNMENTS_ENABLED, Boolean.class).getValue()); - } - - private static EventTopic getBatchEventTopicForAction(final ActionProperties action) { - return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable()) - ? EventTopic.BATCH_DOWNLOAD - : EventTopic.BATCH_DOWNLOAD_AND_INSTALL; - } - - /** - * Creates a Confirmation request. - * @param target the target - * @param actionId the actionId - * @param softwareModules the software modules - * @return - */ - protected DmfConfirmRequest createConfirmRequest(final Target target, final Long actionId, final Map> softwareModules) { - final DmfConfirmRequest request = new DmfConfirmRequest(); - request.setActionId(actionId); - request.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); - - //Software modules can be filtered in the future exposing only the needed. - if (softwareModules != null) { - softwareModules.entrySet() - .forEach(entry -> request.addSoftwareModule(convertToAmqpSoftwareModule(target, entry))); - } - return request; - } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java index 256db0bad..f52c83c53 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -73,38 +73,25 @@ import org.springframework.util.StringUtils; @Slf4j public class AmqpMessageHandlerService extends BaseAmqpService { - private final AmqpMessageDispatcherService amqpMessageDispatcherService; - - private ControllerManagement controllerManagement; - private final ConfirmationManagement confirmationManagement; - - private final EntityFactory entityFactory; - - private final TenantConfigurationManagement tenantConfigurationManagement; - - private final SystemSecurityContext systemSecurityContext; - private static final String THING_ID_NULL = "ThingId is null"; - private static final String EMPTY_MESSAGE_BODY = "\"\""; + private final AmqpMessageDispatcherService amqpMessageDispatcherService; + private final ConfirmationManagement confirmationManagement; + private final EntityFactory entityFactory; + private final TenantConfigurationManagement tenantConfigurationManagement; + private final SystemSecurityContext systemSecurityContext; + private ControllerManagement controllerManagement; /** * Constructor. - * - * @param rabbitTemplate - * for converting messages - * @param amqpMessageDispatcherService - * to sending events to DMF client - * @param controllerManagement - * for target repo access - * @param entityFactory - * to create entities - * @param systemSecurityContext - * the system Security Context - * @param tenantConfigurationManagement - * the tenant configuration Management - * @param confirmationManagement - * the confirmation management + * + * @param rabbitTemplate for converting messages + * @param amqpMessageDispatcherService to sending events to DMF client + * @param controllerManagement for target repo access + * @param entityFactory to create entities + * @param systemSecurityContext the system Security Context + * @param tenantConfigurationManagement the tenant configuration Management + * @param confirmationManagement the confirmation management */ public AmqpMessageHandlerService(final RabbitTemplate rabbitTemplate, final AmqpMessageDispatcherService amqpMessageDispatcherService, @@ -123,12 +110,9 @@ public class AmqpMessageHandlerService extends BaseAmqpService { /** * Method to handle all incoming DMF amqp messages. * - * @param message - * incoming message - * @param type - * the message type - * @param tenant - * the contentType of the message + * @param message incoming message + * @param type the message type + * @param tenant the contentType of the message * @return a message if no message is send back to sender */ @RabbitListener(queues = "${hawkbit.dmf.rabbitmq.receiverQueue:dmf_receiver}", containerFactory = "listenerContainerFactory") @@ -140,15 +124,11 @@ public class AmqpMessageHandlerService extends BaseAmqpService { /** * * Executed if a amqp message arrives. - * - * @param message - * the message - * @param type - * the type - * @param tenant - * the tenant - * @param virtualHost - * the virtual host + * + * @param message the message + * @param type the type + * @param tenant the tenant + * @param virtualHost the virtual host * @return the rpc message back to supplier. */ public Message onMessage(final Message message, final String type, final String tenant, final String virtualHost) { @@ -160,28 +140,28 @@ public class AmqpMessageHandlerService extends BaseAmqpService { try { final MessageType messageType = MessageType.valueOf(type); switch (messageType) { - case THING_CREATED: - setTenantSecurityContext(tenant); - registerTarget(message, virtualHost); - break; - case THING_REMOVED: - setTenantSecurityContext(tenant); - deleteTarget(message); - break; - case EVENT: - checkContentTypeJson(message); - setTenantSecurityContext(tenant); - handleIncomingEvent(message); - break; - case PING: - if (isCorrelationIdNotEmpty(message)) { - amqpMessageDispatcherService.sendPingReponseToDmfReceiver(message, tenant, virtualHost); - } - break; - default: - logAndThrowMessageError(message, "No handle method was found for the given message type."); + case THING_CREATED: + setTenantSecurityContext(tenant); + registerTarget(message, virtualHost); + break; + case THING_REMOVED: + setTenantSecurityContext(tenant); + deleteTarget(message); + break; + case EVENT: + checkContentTypeJson(message); + setTenantSecurityContext(tenant); + handleIncomingEvent(message); + break; + case PING: + if (isCorrelationIdNotEmpty(message)) { + amqpMessageDispatcherService.sendPingReponseToDmfReceiver(message, tenant, virtualHost); + } + break; + default: + logAndThrowMessageError(message, "No handle method was found for the given message type."); } - } catch(AssignmentQuotaExceededException ex) { + } catch (AssignmentQuotaExceededException ex) { throw new AmqpRejectAndDontRequeueException("Could not handle message due to quota violation!", ex); } catch (final IllegalArgumentException ex) { throw new AmqpRejectAndDontRequeueException("Invalid message!", ex); @@ -191,6 +171,11 @@ public class AmqpMessageHandlerService extends BaseAmqpService { return null; } + // for testing + public void setControllerManagement(final ControllerManagement controllerManagement) { + this.controllerManagement = controllerManagement; + } + private static void setSecurityContext(final Authentication authentication) { final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); securityContextImpl.setAuthentication(authentication); @@ -205,15 +190,92 @@ public class AmqpMessageHandlerService extends BaseAmqpService { setSecurityContext(authenticationToken); } + private static boolean isOptionalMessageBodyEmpty(final Message message) { + // empty byte array message body is serialized to double-quoted string + // by message converter and should also be considered as empty + return isMessageBodyEmpty(message) || EMPTY_MESSAGE_BODY.equals(new String(message.getBody())); + } + + private static boolean shouldTargetProceed(final Action action) { + return !action.isActive() || (action.hasMaintenanceSchedule() && action.isMaintenanceWindowAvailable()); + } + + private static boolean isCorrelationIdNotEmpty(final Message message) { + return StringUtils.hasLength(message.getMessageProperties().getCorrelationId()); + } + + // Exception squid:MethodCyclomaticComplexity - false positive, is a simple + // mapping + @SuppressWarnings("squid:MethodCyclomaticComplexity") + private static Status mapStatus(final Message message, final DmfActionUpdateStatus actionUpdateStatus, + final Action action) { + Status status = null; + switch (actionUpdateStatus.getActionStatus()) { + case DOWNLOAD: + status = Status.DOWNLOAD; + break; + case RETRIEVED: + status = Status.RETRIEVED; + break; + case RUNNING: + case CONFIRMED: + status = Status.RUNNING; + break; + case CANCELED: + status = Status.CANCELED; + break; + case FINISHED: + status = Status.FINISHED; + break; + case ERROR: + status = Status.ERROR; + break; + case WARNING: + status = Status.WARNING; + break; + case DOWNLOADED: + status = Status.DOWNLOADED; + break; + case CANCEL_REJECTED: + status = handleCancelRejectedState(message, action); + break; + case DENIED: + status = Status.WAIT_FOR_CONFIRMATION; + break; + default: + logAndThrowMessageError(message, "Status for action does not exisit."); + } + + return status; + } + + private static Status handleCancelRejectedState(final Message message, final Action action) { + if (action.isCancelingOrCanceled()) { + return Status.CANCEL_REJECTED; + } + logAndThrowMessageError(message, + "Cancel rejected message is not allowed, if action is on state: " + action.getStatus()); + return null; + } + + /** + * Retrieve the update mode from the given update message. + */ + private static UpdateMode getUpdateMode(final DmfAttributeUpdate update) { + final DmfUpdateMode mode = update.getMode(); + if (mode != null) { + return UpdateMode.valueOf(mode.name()); + } + return null; + } + /** * Method to create a new target or to find the target if it already exists * and update its poll time, status and optionally its name and attributes. * - * @param message - * the message that contains replyTo property and optionally the - * name and attributes in body - * @param virtualHost - * the virtual host + * @param message the message that contains replyTo property and optionally the + * name and attributes in body + * @param virtualHost the virtual host */ private void registerTarget(final Message message, final String virtualHost) { final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL); @@ -235,7 +297,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { final DmfAttributeUpdate thingAttributeUpdateBody = thingCreateBody.getAttributeUpdate(); log.debug("Received \"THING_CREATED\" AMQP message for thing \"{}\" with target name \"{}\" and type " + - "\"{}\".", thingId, thingCreateBody.getName(), thingCreateBody.getType()); + "\"{}\".", thingId, thingCreateBody.getName(), thingCreateBody.getType()); target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri, thingCreateBody.getName(), thingCreateBody.getType()); @@ -253,12 +315,6 @@ public class AmqpMessageHandlerService extends BaseAmqpService { } } - private static boolean isOptionalMessageBodyEmpty(final Message message) { - // empty byte array message body is serialized to double-quoted string - // by message converter and should also be considered as empty - return isMessageBodyEmpty(message) || EMPTY_MESSAGE_BODY.equals(new String(message.getBody())); - } - private void sendUpdateCommandToTarget(final Target target) { if (isMultiAssignmentsEnabled()) { sendCurrentActionsAsMultiActionToTarget(target); @@ -314,23 +370,22 @@ public class AmqpMessageHandlerService extends BaseAmqpService { /** * Method to handle the different topics to an event. * - * @param message - * the incoming event message. + * @param message the incoming event message. */ private void handleIncomingEvent(final Message message) { switch (EventTopic.valueOf(getStringHeaderKey(message, MessageHeaderKey.TOPIC, "EventTopic is null"))) { - case UPDATE_ACTION_STATUS: - updateActionStatus(message); - break; - case UPDATE_ATTRIBUTES: - updateAttributes(message); - break; - case UPDATE_AUTO_CONFIRM: - setAutoConfirmationState(message); - break; - default: - logAndThrowMessageError(message, "Got event without appropriate topic."); - break; + case UPDATE_ACTION_STATUS: + updateActionStatus(message); + break; + case UPDATE_ATTRIBUTES: + updateAttributes(message); + break; + case UPDATE_AUTO_CONFIRM: + setAutoConfirmationState(message); + break; + default: + logAndThrowMessageError(message, "Got event without appropriate topic."); + break; } } @@ -345,7 +400,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL); controllerManagement.updateControllerAttributes(thingId, attributeUpdate.getAttributes(), - getUpdateMode(attributeUpdate)); + getUpdateMode(attributeUpdate)); } private void setAutoConfirmationState(final Message message) { @@ -367,8 +422,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { /** * Method to update the action status of an action through the event. * - * @param message - * the object form the ampq message + * @param message the object form the ampq message */ private void updateActionStatus(final Message message) { final DmfActionUpdateStatus actionUpdateStatus = convertMessage(message, DmfActionUpdateStatus.class); @@ -408,68 +462,6 @@ public class AmqpMessageHandlerService extends BaseAmqpService { } } - private static boolean shouldTargetProceed(final Action action) { - return !action.isActive() || (action.hasMaintenanceSchedule() && action.isMaintenanceWindowAvailable()); - } - - private static boolean isCorrelationIdNotEmpty(final Message message) { - return StringUtils.hasLength(message.getMessageProperties().getCorrelationId()); - } - - // Exception squid:MethodCyclomaticComplexity - false positive, is a simple - // mapping - @SuppressWarnings("squid:MethodCyclomaticComplexity") - private static Status mapStatus(final Message message, final DmfActionUpdateStatus actionUpdateStatus, - final Action action) { - Status status = null; - switch (actionUpdateStatus.getActionStatus()) { - case DOWNLOAD: - status = Status.DOWNLOAD; - break; - case RETRIEVED: - status = Status.RETRIEVED; - break; - case RUNNING: - case CONFIRMED: - status = Status.RUNNING; - break; - case CANCELED: - status = Status.CANCELED; - break; - case FINISHED: - status = Status.FINISHED; - break; - case ERROR: - status = Status.ERROR; - break; - case WARNING: - status = Status.WARNING; - break; - case DOWNLOADED: - status = Status.DOWNLOADED; - break; - case CANCEL_REJECTED: - status = handleCancelRejectedState(message, action); - break; - case DENIED: - status = Status.WAIT_FOR_CONFIRMATION; - break; - default: - logAndThrowMessageError(message, "Status for action does not exisit."); - } - - return status; - } - - private static Status handleCancelRejectedState(final Message message, final Action action) { - if (action.isCancelingOrCanceled()) { - return Status.CANCEL_REJECTED; - } - logAndThrowMessageError(message, - "Cancel rejected message is not allowed, if action is on state: " + action.getStatus()); - return null; - } - // Exception squid:S3655 - logAndThrowMessageError throws exception, i.e. // get will not be called @SuppressWarnings("squid:S3655") @@ -488,17 +480,6 @@ public class AmqpMessageHandlerService extends BaseAmqpService { return findActionWithDetails.get(); } - /** - * Retrieve the update mode from the given update message. - */ - private static UpdateMode getUpdateMode(final DmfAttributeUpdate update) { - final DmfUpdateMode mode = update.getMode(); - if (mode != null) { - return UpdateMode.valueOf(mode.name()); - } - return null; - } - private boolean isMultiAssignmentsEnabled() { return getConfigValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class); } @@ -507,9 +488,4 @@ public class AmqpMessageHandlerService extends BaseAmqpService { return systemSecurityContext .runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue()); } - - // for testing - public void setControllerManagement(final ControllerManagement controllerManagement) { - this.controllerManagement = controllerManagement; - } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageSenderService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageSenderService.java index d9ff859c2..7e6b519e9 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageSenderService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageSenderService.java @@ -24,11 +24,9 @@ public interface AmqpMessageSenderService { /** * Send the given message to the given uri. The uri contains the (virtual) * host and exchange e.g amqp://host/exchange. - * - * @param message - * the amqp message - * @param replyTo - * the reply to uri + * + * @param message the amqp message + * @param replyTo the reply to uri */ void sendMessage(@NotNull final Message message, @NotNull final URI replyTo); } \ No newline at end of file diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpProperties.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpProperties.java index 02d510d74..844a6bafb 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpProperties.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpProperties.java @@ -15,7 +15,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * Bean which holds the necessary properties for configuring the AMQP * connection. - * */ @Data @ConfigurationProperties("hawkbit.dmf.rabbitmq") diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/BaseAmqpService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/BaseAmqpService.java index 51c90a6e7..c08832a19 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/BaseAmqpService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/BaseAmqpService.java @@ -32,29 +32,18 @@ public class BaseAmqpService { /** * Constructor. - * - * @param rabbitTemplate - * the rabbit template. + * + * @param rabbitTemplate the rabbit template. */ public BaseAmqpService(final RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } - protected static void checkContentTypeJson(final Message message) { - final MessageProperties messageProperties = message.getMessageProperties(); - if (messageProperties.getContentType() != null && messageProperties.getContentType().contains("json")) { - return; - } - throw new AmqpRejectAndDontRequeueException("Content-Type is not JSON compatible"); - } - /** * Is needed to convert a incoming message to is originally object type. * - * @param message - * the message to convert. - * @param clazz - * the class of the originally object. + * @param message the message to convert. + * @param clazz the class of the originally object. * @return the converted object */ @SuppressWarnings("unchecked") @@ -65,14 +54,27 @@ public class BaseAmqpService { return (T) rabbitTemplate.getMessageConverter().fromMessage(message); } - protected MessageConverter getMessageConverter() { - return rabbitTemplate.getMessageConverter(); + protected static void checkContentTypeJson(final Message message) { + final MessageProperties messageProperties = message.getMessageProperties(); + if (messageProperties.getContentType() != null && messageProperties.getContentType().contains("json")) { + return; + } + throw new AmqpRejectAndDontRequeueException("Content-Type is not JSON compatible"); } protected static boolean isMessageBodyEmpty(final Message message) { return message.getBody() == null || message.getBody().length == 0; } + protected static final void logAndThrowMessageError(final Message message, final String error) { + log.debug("Warning! \"{}\" reported by message: {}", error, message); + throw new AmqpRejectAndDontRequeueException(error); + } + + protected MessageConverter getMessageConverter() { + return rabbitTemplate.getMessageConverter(); + } + protected void checkMessageBody(@NotNull final Message message) { if (isMessageBodyEmpty(message)) { throw new MessageConversionException("Message body cannot be null"); @@ -89,20 +91,14 @@ public class BaseAmqpService { return value.toString(); } - protected static final void logAndThrowMessageError(final Message message, final String error) { - log.debug("Warning! \"{}\" reported by message: {}", error, message); - throw new AmqpRejectAndDontRequeueException(error); - } - protected RabbitTemplate getRabbitTemplate() { return rabbitTemplate; } /** * Clean message properties before sending a message. - * - * @param message - * the message to cleaned up + * + * @param message the message to cleaned up */ protected void cleanMessageHeaderProperties(final Message message) { message.getMessageProperties().getHeaders().remove(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/ConfigurableRabbitListenerContainerFactory.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/ConfigurableRabbitListenerContainerFactory.java index 662a97d92..b0abfbb83 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/ConfigurableRabbitListenerContainerFactory.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/ConfigurableRabbitListenerContainerFactory.java @@ -18,7 +18,6 @@ import org.springframework.util.ErrorHandler; /** * {@link RabbitListenerContainerFactory} that can be configured through * hawkBit's {@link AmqpProperties}. - * */ public class ConfigurableRabbitListenerContainerFactory extends SimpleRabbitListenerContainerFactory { @@ -26,14 +25,11 @@ public class ConfigurableRabbitListenerContainerFactory extends SimpleRabbitList /** * Constructor. - * - * @param missingQueuesFatal - * the missingQueuesFatal to set. + * + * @param missingQueuesFatal the missingQueuesFatal to set. + * @param declarationRetries The number of retries + * @param errorHandler the error handler which should be use * @see SimpleMessageListenerContainer#setMissingQueuesFatal - * @param declarationRetries - * The number of retries - * @param errorHandler - * the error handler which should be use */ public ConfigurableRabbitListenerContainerFactory(final boolean missingQueuesFatal, final int declarationRetries, final ErrorHandler errorHandler) { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DefaultAmqpMessageSenderService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DefaultAmqpMessageSenderService.java index 2c6abedb7..0fbf52d66 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DefaultAmqpMessageSenderService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DefaultAmqpMessageSenderService.java @@ -29,7 +29,7 @@ public class DefaultAmqpMessageSenderService extends BaseAmqpService implements /** * Constructor. - * + * * @param rabbitTemplate the AMQP template */ public DefaultAmqpMessageSenderService(final RabbitTemplate rabbitTemplate) { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelayedRequeueExceptionStrategy.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelayedRequeueExceptionStrategy.java index 41078d884..0fb7c3d80 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelayedRequeueExceptionStrategy.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelayedRequeueExceptionStrategy.java @@ -14,11 +14,11 @@ import java.util.concurrent.TimeUnit; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAddressException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; -import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.TenantNotExistException; import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; import org.springframework.amqp.rabbit.listener.FatalExceptionStrategy; diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelegatingConditionalErrorHandler.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelegatingConditionalErrorHandler.java index 5dc2c08c6..ccd0b6489 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelegatingConditionalErrorHandler.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DelegatingConditionalErrorHandler.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.amqp; import java.util.List; + import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; @@ -21,16 +22,15 @@ import org.springframework.util.ErrorHandler; */ @Slf4j public class DelegatingConditionalErrorHandler implements ErrorHandler { + private final List handlers; private final ErrorHandler defaultHandler; /** * Constructor * - * @param handlers - * {@link List} of error handlers - * @param defaultHandler - * the default error handler + * @param handlers {@link List} of error handlers + * @param defaultHandler the default error handler */ public DelegatingConditionalErrorHandler(final List handlers, @NotNull final ErrorHandler defaultHandler) { this.handlers = handlers; @@ -53,7 +53,7 @@ public class DelegatingConditionalErrorHandler implements ErrorHandler { } private boolean includesAmqpRejectException(final Throwable t) { - if (t instanceof AmqpRejectAndDontRequeueException){ + if (t instanceof AmqpRejectAndDontRequeueException) { return true; } if (t.getCause() != null) { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/MessageConversionExceptionHandler.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/MessageConversionExceptionHandler.java index 46f5a819c..6c05dd3a6 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/MessageConversionExceptionHandler.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/MessageConversionExceptionHandler.java @@ -30,11 +30,11 @@ public class MessageConversionExceptionHandler extends AbstractAmqpErrorHandler< //since the detailed error message lies in the first parent of current throwable we retrieve it // and append it to the errorMessage final Optional detailedErrorMessage = getFirstAncestralErrorMessage(throwable.getCause()); - return detailedErrorMessage.isPresent()? (detailedErrorMessage.get() + errorMessage) : errorMessage; + return detailedErrorMessage.isPresent() ? (detailedErrorMessage.get() + errorMessage) : errorMessage; } private Optional getFirstAncestralErrorMessage(final Throwable throwable) { - if(throwable.getCause() instanceof InvalidFormatException) { + if (throwable.getCause() instanceof InvalidFormatException) { return Optional.of(throwable.getCause().getMessage()); } return Optional.empty(); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java index f907347c0..cdb8a3143 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java @@ -23,6 +23,9 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; import org.eclipse.hawkbit.api.ArtifactUrl; import org.eclipse.hawkbit.api.ArtifactUrlHandler; import org.eclipse.hawkbit.artifact.repository.ArtifactFilesystem; @@ -33,7 +36,6 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; import org.eclipse.hawkbit.dmf.amqp.api.MessageType; import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; -import org.eclipse.hawkbit.dmf.json.model.DmfMetadata; import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent; @@ -62,10 +64,6 @@ import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import io.qameta.allure.Description; -import io.qameta.allure.Feature; -import io.qameta.allure.Story; - @ActiveProfiles({ "test" }) @Feature("Component Tests - Device Management Federation API") @Story("AmqpMessage Dispatcher Service Test") @@ -117,14 +115,10 @@ class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { } - private Message getCaptureAddressEvent(final TargetAssignDistributionSetEvent targetAssignDistributionSetEvent) { - final Target target = targetManagement - .getByControllerID(targetAssignDistributionSetEvent.getActions().keySet().iterator().next()).get(); - return createArgumentCapture(target.getAddress()); - } - - private Action createAction(final DistributionSet testDs) { - return getFirstAssignedAction(assignDistributionSet(testDs, testTarget)); + protected Message createArgumentCapture(final URI uri) { + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Message.class); + Mockito.verify(senderService).sendMessage(argumentCaptor.capture(), eq(uri)); + return argumentCaptor.getValue(); } @Test @@ -295,6 +289,16 @@ class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { Mockito.verifyNoInteractions(senderService); } + private Message getCaptureAddressEvent(final TargetAssignDistributionSetEvent targetAssignDistributionSetEvent) { + final Target target = targetManagement + .getByControllerID(targetAssignDistributionSetEvent.getActions().keySet().iterator().next()).get(); + return createArgumentCapture(target.getAddress()); + } + + private Action createAction(final DistributionSet testDs) { + return getFirstAssignedAction(assignDistributionSet(testDs, testTarget)); + } + private void assertCancelMessage(final Message sendMessage) { assertEventMessage(sendMessage); final DmfActionRequest actionId = convertMessage(sendMessage, DmfActionRequest.class); @@ -313,7 +317,7 @@ class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { assertThat(sendMessage.getMessageProperties().getHeaders()) .containsEntry(MessageHeaderKey.TENANT, TENANT); assertThat(sendMessage.getMessageProperties().getHeaders()) - .containsEntry(MessageHeaderKey.TYPE,MessageType.THING_DELETED); + .containsEntry(MessageHeaderKey.TYPE, MessageType.THING_DELETED); } private DmfDownloadAndUpdateRequest assertDownloadAndInstallMessage(final Message sendMessage, final Long action) { @@ -323,7 +327,7 @@ class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { assertThat(downloadAndUpdateRequest.getActionId()).isEqualTo(action); assertThat(sendMessage.getMessageProperties().getHeaders()) .as("The topic of the event should contain DOWNLOAD_AND_INSTALL") - .containsEntry(MessageHeaderKey.TOPIC,EventTopic.DOWNLOAD_AND_INSTALL); + .containsEntry(MessageHeaderKey.TOPIC, EventTopic.DOWNLOAD_AND_INSTALL); assertThat(downloadAndUpdateRequest.getTargetSecurityToken()) .as("Security token of target") .isEqualTo(TEST_TOKEN); @@ -346,18 +350,12 @@ class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { .containsEntry(MessageHeaderKey.THING_ID, CONTROLLER_ID); assertThat(sendMessage.getMessageProperties().getHeaders()) .as("The value of the message header TYPE should be EVENT") - .containsEntry(MessageHeaderKey.TYPE,MessageType.EVENT); + .containsEntry(MessageHeaderKey.TYPE, MessageType.EVENT); assertThat(sendMessage.getMessageProperties().getContentType()) .as("The content type message should be " + MessageProperties.CONTENT_TYPE_JSON) .isEqualTo(MessageProperties.CONTENT_TYPE_JSON); } - protected Message createArgumentCapture(final URI uri) { - final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Message.class); - Mockito.verify(senderService).sendMessage(argumentCaptor.capture(), eq(uri)); - return argumentCaptor.getValue(); - } - @SuppressWarnings("unchecked") private T convertMessage(final Message message, final Class clazz) { message.getMessageProperties().getHeaders().put(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME, diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index c22bfa839..096672840 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -26,6 +26,10 @@ import java.net.URI; import java.util.Map; import java.util.Optional; +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Step; +import io.qameta.allure.Story; import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; import org.eclipse.hawkbit.dmf.amqp.api.MessageType; @@ -71,11 +75,6 @@ import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; -import io.qameta.allure.Description; -import io.qameta.allure.Feature; -import io.qameta.allure.Step; -import io.qameta.allure.Story; - @ExtendWith(MockitoExtension.class) @Feature("Component Tests - Device Management Federation API") @Story("AmqpMessage Handler Service Test") @@ -168,49 +167,6 @@ public class AmqpMessageHandlerServiceTest { assertReplyToCapturedField("MyTest"); } - @Step - private void processThingCreatedMessage(final String thingId, final DmfCreateThing payload) { - final MessageProperties messageProperties = getThingCreatedMessageProperties(thingId); - final Message message = createMessage(payload != null ? payload : new byte[0], messageProperties); - - final Target targetMock = mock(Target.class); - if (payload == null) { - when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), - uriCaptor.capture())).thenReturn(targetMock); - } else { - when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), - uriCaptor.capture(), targetNameCaptor.capture(), targetTypeNameCaptor.capture())) - .thenReturn(targetMock); - if (payload.getAttributeUpdate() != null) { - when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), - attributesCaptor.capture(), modeCaptor.capture())).thenReturn(null); - } - } - when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.empty()); - - amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, VIRTUAL_HOST); - } - - @Step - private void assertThingIdCapturedField(final String thingId) { - assertThat(targetIdCaptor.getValue()).as("Thing id is wrong").isEqualTo(thingId); - } - - @Step - private void assertReplyToCapturedField(final String replyTo) { - assertThat(uriCaptor.getValue()).as("Uri is not right").hasToString("amqp://" + VIRTUAL_HOST + "/" + replyTo); - } - - @Step - private void assertInitiatorCapturedField(final String initiator) { - assertThat(initiatorCaptor.getValue()).as("Initiator is wrong").isEqualTo(initiator); - } - - @Step - private void assertRemarkCapturedField(final String remark) { - assertThat(remarkCaptor.getValue()).as("Remark is wrong").isEqualTo(remark); - } - @Test @Description("Tests the creation of a target/thing with specified name by calling the same method that incoming RabbitMQ messages would access.") public void createThingWithName() { @@ -227,11 +183,6 @@ public class AmqpMessageHandlerServiceTest { assertThingNameCapturedField(knownThingName); } - @Step - private void assertThingNameCapturedField(final String thingName) { - assertThat(targetNameCaptor.getValue()).as("Thing name is wrong").isEqualTo(thingName); - } - @Test @Description("Tests the creation of a target/thing with specified type name by calling the same method that incoming RabbitMQ messages would access.") public void createThingWithType() { @@ -248,11 +199,6 @@ public class AmqpMessageHandlerServiceTest { assertThingTypeCapturedField(knownThingTypeName); } - @Step - private void assertThingTypeCapturedField(final String thingType) { - assertThat(targetTypeNameCaptor.getValue()).as("Thing type is wrong").isEqualTo(thingType); - } - @Test @Description("Tests not allowed body in message") public void createThingWithWrongBody() { @@ -284,11 +230,6 @@ public class AmqpMessageHandlerServiceTest { assertThingAttributesCapturedField(attributeUpdate.getAttributes()); } - @Step - private void assertThingAttributesCapturedField(final Map attributes) { - assertThat(attributesCaptor.getValue()).as("Attributes is not right").isEqualTo(attributes); - } - @Test @Description("Tests the creation of a target/thing with specified name and attributes by calling the same method that incoming RabbitMQ messages would access.") public void createThingWithNameAndAttributes() { @@ -312,11 +253,6 @@ public class AmqpMessageHandlerServiceTest { assertThingAttributesModeCapturedField(UpdateMode.REPLACE); } - @Step - private void assertThingAttributesModeCapturedField(final UpdateMode attributesUpdateMode) { - assertThat(modeCaptor.getValue()).as("Attributes update mode is not right").isEqualTo(attributesUpdateMode); - } - @Test @Description("Tests the target attribute update by calling the same method that incoming RabbitMQ messages would access.") public void updateAttributes() { @@ -571,6 +507,35 @@ public class AmqpMessageHandlerServiceTest { .contains("Device reported status code: 12"); } + @Test + @Description("Tests the deletion of a target/thing, requested by the target itself.") + public void deleteThing() { + // prepare valid message + final String knownThingId = "1"; + final MessageProperties messageProperties = createMessageProperties(MessageType.THING_REMOVED); + messageProperties.setHeader(MessageHeaderKey.THING_ID, knownThingId); + final Message message = createMessage(new byte[0], messageProperties); + + // test + amqpMessageHandlerService.onMessage(message, MessageType.THING_REMOVED.name(), TENANT, VIRTUAL_HOST); + + // verify + verify(controllerManagementMock).deleteExistingTarget(knownThingId); + } + + @Test + @Description("Tests the deletion of a target/thing with missing thingId") + public void deleteThingWithoutThingId() { + // prepare invalid message + final MessageProperties messageProperties = createMessageProperties(MessageType.THING_REMOVED); + final Message message = createMessage(new byte[0], messageProperties); + + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no thingId was set") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.THING_REMOVED.name(), TENANT, + VIRTUAL_HOST)); + } + @Test @Description("Tests activating auto-confirmation on a target.") void setAutoConfirmationStateActive() { @@ -589,7 +554,7 @@ public class AmqpMessageHandlerServiceTest { final Message message = createMessage(autoConfirmation, messageProperties); when(controllerManagementMock.activateAutoConfirmation(targetIdCaptor.capture(), initiatorCaptor.capture(), - remarkCaptor.capture())).thenReturn(null); + remarkCaptor.capture())).thenReturn(null); amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); @@ -600,7 +565,6 @@ public class AmqpMessageHandlerServiceTest { assertRemarkCapturedField(remark); } - @Test @Description("Tests deactivating auto-confirmation on a target.") void setAutoConfirmationStateDeactivated() { @@ -621,6 +585,69 @@ public class AmqpMessageHandlerServiceTest { assertThingIdCapturedField(knownThingId); } + @Step + private void processThingCreatedMessage(final String thingId, final DmfCreateThing payload) { + final MessageProperties messageProperties = getThingCreatedMessageProperties(thingId); + final Message message = createMessage(payload != null ? payload : new byte[0], messageProperties); + + final Target targetMock = mock(Target.class); + if (payload == null) { + when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), + uriCaptor.capture())).thenReturn(targetMock); + } else { + when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), + uriCaptor.capture(), targetNameCaptor.capture(), targetTypeNameCaptor.capture())) + .thenReturn(targetMock); + if (payload.getAttributeUpdate() != null) { + when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), + attributesCaptor.capture(), modeCaptor.capture())).thenReturn(null); + } + } + when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.empty()); + + amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, VIRTUAL_HOST); + } + + @Step + private void assertThingIdCapturedField(final String thingId) { + assertThat(targetIdCaptor.getValue()).as("Thing id is wrong").isEqualTo(thingId); + } + + @Step + private void assertReplyToCapturedField(final String replyTo) { + assertThat(uriCaptor.getValue()).as("Uri is not right").hasToString("amqp://" + VIRTUAL_HOST + "/" + replyTo); + } + + @Step + private void assertInitiatorCapturedField(final String initiator) { + assertThat(initiatorCaptor.getValue()).as("Initiator is wrong").isEqualTo(initiator); + } + + @Step + private void assertRemarkCapturedField(final String remark) { + assertThat(remarkCaptor.getValue()).as("Remark is wrong").isEqualTo(remark); + } + + @Step + private void assertThingNameCapturedField(final String thingName) { + assertThat(targetNameCaptor.getValue()).as("Thing name is wrong").isEqualTo(thingName); + } + + @Step + private void assertThingTypeCapturedField(final String thingType) { + assertThat(targetTypeNameCaptor.getValue()).as("Thing type is wrong").isEqualTo(thingType); + } + + @Step + private void assertThingAttributesCapturedField(final Map attributes) { + assertThat(attributesCaptor.getValue()).as("Attributes is not right").isEqualTo(attributes); + } + + @Step + private void assertThingAttributesModeCapturedField(final UpdateMode attributesUpdateMode) { + assertThat(modeCaptor.getValue()).as("Attributes update mode is not right").isEqualTo(attributesUpdateMode); + } + private DmfActionUpdateStatus createActionUpdateStatus(final DmfActionStatus status) { return createActionUpdateStatus(status, 2L); } @@ -675,35 +702,6 @@ public class AmqpMessageHandlerServiceTest { } } - @Test - @Description("Tests the deletion of a target/thing, requested by the target itself.") - public void deleteThing() { - // prepare valid message - final String knownThingId = "1"; - final MessageProperties messageProperties = createMessageProperties(MessageType.THING_REMOVED); - messageProperties.setHeader(MessageHeaderKey.THING_ID, knownThingId); - final Message message = createMessage(new byte[0], messageProperties); - - // test - amqpMessageHandlerService.onMessage(message, MessageType.THING_REMOVED.name(), TENANT, VIRTUAL_HOST); - - // verify - verify(controllerManagementMock).deleteExistingTarget(knownThingId); - } - - @Test - @Description("Tests the deletion of a target/thing with missing thingId") - public void deleteThingWithoutThingId() { - // prepare invalid message - final MessageProperties messageProperties = createMessageProperties(MessageType.THING_REMOVED); - final Message message = createMessage(new byte[0], messageProperties); - - assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) - .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no thingId was set") - .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.THING_REMOVED.name(), TENANT, - VIRTUAL_HOST)); - } - private MessageProperties getThingCreatedMessageProperties(final String thingId) { final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); messageProperties.setHeader(MessageHeaderKey.THING_ID, thingId); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java index fb16293ac..189ede489 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java @@ -13,6 +13,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.when; +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; @@ -29,10 +32,6 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConversionException; -import io.qameta.allure.Description; -import io.qameta.allure.Feature; -import io.qameta.allure.Story; - @ExtendWith(MockitoExtension.class) @Feature("Component Tests - Device Management Federation API") @Story("Base Amqp Service Test") diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/DelegatingAmqpErrorHandlerTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/DelegatingAmqpErrorHandlerTest.java index b979c9194..ccd0d6aa9 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/DelegatingAmqpErrorHandlerTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/DelegatingAmqpErrorHandlerTest.java @@ -11,35 +11,34 @@ package org.eclipse.hawkbit.amqp; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.util.ArrayList; +import java.util.List; + import io.qameta.allure.Description; import io.qameta.allure.Feature; import io.qameta.allure.Story; - import org.junit.jupiter.api.Test; import org.springframework.util.ErrorHandler; -import java.util.ArrayList; -import java.util.List; - @Feature("Unit Tests - Delegating Conditional Error Handler") @Story("Delegating Conditional Error Handler") public class DelegatingAmqpErrorHandlerTest { @Test @Description("Verifies that with a list of conditional error handlers, the error is delegated to specific handler.") - public void verifyDelegationHandling(){ + public void verifyDelegationHandling() { List handlers = new ArrayList<>(); handlers.add(new IllegalArgumentExceptionHandler()); handlers.add(new IndexOutOfBoundsExceptionHandler()); assertThatExceptionOfType(IllegalArgumentException.class) .as("Expected handled exception to be of type IllegalArgumentException") .isThrownBy(() -> new DelegatingConditionalErrorHandler(handlers, new DefaultErrorHandler()) - .handleError(new Throwable(new IllegalArgumentException()))); + .handleError(new Throwable(new IllegalArgumentException()))); } @Test @Description("Verifies that default handler is used if no handlers are defined for the specific exception.") - public void verifyDefaultDelegationHandling(){ + public void verifyDefaultDelegationHandling() { List handlers = new ArrayList<>(); handlers.add(new IllegalArgumentExceptionHandler()); handlers.add(new IndexOutOfBoundsExceptionHandler()); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java index 0a89d6df7..9aaa34234 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java @@ -21,6 +21,8 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; +import com.cronutils.utils.StringUtils; +import io.qameta.allure.Step; import org.assertj.core.api.HamcrestCondition; import org.eclipse.hawkbit.amqp.DmfApiConfiguration; import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings; @@ -48,9 +50,8 @@ import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; -import org.eclipse.hawkbit.repository.test.TestConfiguration; -import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch; +import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.util.IpUtil; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mockito; @@ -63,12 +64,7 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ContextConfiguration; import org.springframework.util.CollectionUtils; -import com.cronutils.utils.StringUtils; - -import io.qameta.allure.Step; - /** - * * Common class for {@link AmqpMessageHandlerServiceIntegrationTest} and * {@link AmqpMessageDispatcherServiceIntegrationTest}. */ @@ -131,7 +127,7 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt testdataFactory.addSoftwareModuleMetadata(distributionSet); return registerTargetAndAssignDistributionSet(distributionSet.getId(), TargetUpdateStatus.REGISTERED, - distributionSet.getModules(), controllerId); + distributionSet.getModules(), controllerId); } protected DistributionSetAssignmentResult prepareDistributionSetAndAssign(final String controllerId) { @@ -196,17 +192,6 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt } - private void assertAssignmentMessage(final Set dsModules, final String controllerId, - final EventTopic topic) { - final Message replyMessage = assertReplyMessageHeader(topic, controllerId); - assertAllTargetsCount(1); - - final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = (DmfDownloadAndUpdateRequest) getDmfClient() - .getMessageConverter().fromMessage(replyMessage); - - assertDmfDownloadAndUpdateRequest(downloadAndUpdateRequest, dsModules, controllerId); - } - protected void assertDmfDownloadAndUpdateRequest(final DmfDownloadAndUpdateRequest request, final Set softwareModules, final String controllerId) { assertSoftwareModules(softwareModules, request.getSoftwareModules()); @@ -294,18 +279,6 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt expectedTargetStatus, createdBy, attributes, () -> targetManagement.getByControllerID(controllerId)); } - private void registerAndAssertTargetWithExistingTenant(final String controllerId, final String name, - final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus, - final String createdBy, final Map attributes, - final Callable> fetchTarget) { - createAndSendThingCreated(controllerId, name, attributes); - final Target registeredTarget = waitUntilIsPresent(fetchTarget::call); - assertAllTargetsCount(existingTargetsAfterCreation); - assertThat(registeredTarget).isNotNull(); - assertTarget(registeredTarget, name != null ? name : controllerId, expectedTargetStatus, createdBy, - attributes != null ? attributes : Collections.emptyMap()); - } - protected void registerSameTargetAndAssertBasedOnVersion(final String controllerId, final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus) { registerSameTargetAndAssertBasedOnVersion(controllerId, null, existingTargetsAfterCreation, @@ -320,27 +293,6 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt expectedTargetStatus, CREATED_BY, attributes, () -> findTargetBasedOnNewVersion(controllerId, version)); } - private Optional findTargetBasedOnNewVersion(final String controllerId, final int version) { - final Optional target2 = controllerManagement.getByControllerId(controllerId); - if (version < target2.get().getOptLockRevision()) { - return target2; - } - return Optional.empty(); - } - - private void assertTarget(final Target target, final String name, final TargetUpdateStatus updateStatus, - final String createdBy, final Map attributes) { - assertThat(target.getTenant()).isEqualTo(TENANT_EXIST); - assertThat(target.getName()).isEqualTo(name); - assertThat(target.getDescription()).contains("Plug and Play"); - assertThat(target.getDescription()).contains(target.getControllerId()); - assertThat(target.getCreatedBy()).isEqualTo(createdBy); - assertThat(target.getUpdateStatus()).isEqualTo(updateStatus); - assertThat(target.getAddress()) - .isEqualTo(IpUtil.createAmqpUri(getVirtualHost(), DmfTestConfiguration.REPLY_TO_EXCHANGE)); - assertThat(targetManagement.getControllerAttributes(target.getControllerId())).isEqualTo(attributes); - } - protected Message createTargetMessage(final String controllerId, final String tenant) { return createTargetMessage(controllerId, null, null, tenant); } @@ -458,7 +410,7 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt } protected void assertSoftwareModules(final Set expectedSoftwareModules, - final List softwareModules) { + final List softwareModules) { assertThat(expectedSoftwareModules) .is(new HamcrestCondition<>(SoftwareModuleJsonMatcher.containsExactly(softwareModules))); softwareModules.forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly( @@ -484,4 +436,48 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt assertThat(updatedTarget.getSecurityToken()).isEqualTo(request.getTargetSecurityToken()); } + private void assertAssignmentMessage(final Set dsModules, final String controllerId, + final EventTopic topic) { + final Message replyMessage = assertReplyMessageHeader(topic, controllerId); + assertAllTargetsCount(1); + + final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = (DmfDownloadAndUpdateRequest) getDmfClient() + .getMessageConverter().fromMessage(replyMessage); + + assertDmfDownloadAndUpdateRequest(downloadAndUpdateRequest, dsModules, controllerId); + } + + private void registerAndAssertTargetWithExistingTenant(final String controllerId, final String name, + final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus, + final String createdBy, final Map attributes, + final Callable> fetchTarget) { + createAndSendThingCreated(controllerId, name, attributes); + final Target registeredTarget = waitUntilIsPresent(fetchTarget::call); + assertAllTargetsCount(existingTargetsAfterCreation); + assertThat(registeredTarget).isNotNull(); + assertTarget(registeredTarget, name != null ? name : controllerId, expectedTargetStatus, createdBy, + attributes != null ? attributes : Collections.emptyMap()); + } + + private Optional findTargetBasedOnNewVersion(final String controllerId, final int version) { + final Optional target2 = controllerManagement.getByControllerId(controllerId); + if (version < target2.get().getOptLockRevision()) { + return target2; + } + return Optional.empty(); + } + + private void assertTarget(final Target target, final String name, final TargetUpdateStatus updateStatus, + final String createdBy, final Map attributes) { + assertThat(target.getTenant()).isEqualTo(TENANT_EXIST); + assertThat(target.getName()).isEqualTo(name); + assertThat(target.getDescription()).contains("Plug and Play"); + assertThat(target.getDescription()).contains(target.getControllerId()); + assertThat(target.getCreatedBy()).isEqualTo(createdBy); + assertThat(target.getUpdateStatus()).isEqualTo(updateStatus); + assertThat(target.getAddress()) + .isEqualTo(IpUtil.createAmqpUri(getVirtualHost(), DmfTestConfiguration.REPLY_TO_EXCHANGE)); + assertThat(targetManagement.getControllerAttributes(target.getControllerId())).isEqualTo(attributes); + } + } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java index fcabe5e25..8686b49b0 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java @@ -30,6 +30,9 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.stream.Collectors; +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest; @@ -80,13 +83,10 @@ import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mockito; import org.springframework.amqp.core.Message; -import io.qameta.allure.Description; -import io.qameta.allure.Feature; -import io.qameta.allure.Story; - @Feature("Component Tests - Device Management Federation API") @Story("Amqp Message Dispatcher Service") public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpServiceIntegrationTest { + private static final String TARGET_PREFIX = "Dmf_disp_"; @Test @@ -140,7 +140,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) - public void sendDownloadAndInstallStatusMessageDuringMaintenanceWindow() { + public void sendDownloadAndInstallStatusMessageDuringMaintenanceWindow() { final String controllerId = TARGET_PREFIX + "sendDAndIStatusMessageDuringWindow"; registerAndAssertTargetWithExistingTenant(controllerId); @@ -242,7 +242,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer .setActionType(ActionType.DOWNLOAD_ONLY).setWeight(weight1).build()).getAssignedEntity().get(0).getId(); final Long cancelActionId = makeAssignment( DeploymentManagement.deploymentRequest(controllerId, ds.getId()).setWeight(DEFAULT_TEST_WEIGHT).build()) - .getAssignedEntity().get(0).getId(); + .getAssignedEntity().get(0).getId(); // make sure the latest message in the queue is the one triggered by the // cancellation waitUntilEventMessagesAreDispatchedToTarget(EventTopic.DOWNLOAD_AND_INSTALL, EventTopic.MULTI_ACTION, @@ -274,14 +274,6 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer controllerId); } - private List getLatestMultiActionMessages(final String expectedControllerId) { - final Message multiactionMessage = replyToListener.getLatestEventMessage(EventTopic.MULTI_ACTION); - assertThat(multiactionMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.THING_ID)) - .isEqualTo(expectedControllerId); - return ((DmfMultiActionRequest) getDmfClient().getMessageConverter().fromMessage(multiactionMessage)) - .getElements(); - } - @Test @Description("Handle cancelation process of an action in multi assignment mode.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @@ -376,24 +368,6 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer assertLatestMultiActionMessage(controllerId, Arrays.asList(action1Install, action2Install)); } - private void updateActionViaDmfClient(final String controllerId, final long actionId, - final DmfActionStatus status) { - createAndSendActionStatusUpdateMessage(controllerId, actionId, status); - } - - private Long assignNewDsToTarget(final String controllerId) { - return assignNewDsToTarget(controllerId, null); - } - - private Long assignNewDsToTarget(final String controllerId, final Integer weight) { - final DistributionSet ds = testdataFactory.createDistributionSet(); - final Long actionId = getFirstAssignedActionId( - assignDistributionSet(ds.getId(), Collections.singletonList(controllerId), ActionType.FORCED, - RepositoryModelConstants.NO_FORCE_TIME, weight)); - waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); - return actionId; - } - @Test @Description("If multi assignment is enabled multiple rollouts with the same DS lead to multiple actions.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @@ -474,18 +448,6 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer assertLatestMultiActionMessageContainsInstallMessages(controllerId, Collections.singletonList(smIds1)); } - private Set getSoftwareModuleIds(final DistributionSet ds) { - return ds.getModules().stream().map(SoftwareModule::getId).collect(Collectors.toSet()); - } - - private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery, final Integer weight) { - final Rollout rollout = testdataFactory.createRolloutByVariables(UUID.randomUUID().toString(), "", 1, - filterQuery, ds, "50", "5", ActionType.FORCED, weight, false); - rolloutManagement.start(rollout.getId()); - rolloutHandler.handleAll(); - return rollout; - } - @Test @Description("Verify that a cancel assignment send a cancel message.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @@ -577,6 +539,169 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer assertThat(assignedDistributionSet.getId()).isEqualTo(distributionSet.getId()); } + @Test + @Description("Verify payload of batch assignment download and install message.") + public void assertBatchAssignmentsDownloadAndInstall() { + assertBatchAssignmentsMessagePayload(BATCH_DOWNLOAD_AND_INSTALL); + } + + @Test + @Description("Verify payload of batch assignments download only message.") + public void assertBatchAssignmentsDownloadOnly() { + assertBatchAssignmentsMessagePayload(BATCH_DOWNLOAD); + } + + protected void assertDmfBatchDownloadAndUpdateRequest(final DmfBatchDownloadAndUpdateRequest request, + final Set softwareModules, + final List controllerIds) { + assertSoftwareModules(softwareModules, request.getSoftwareModules()); + + final List tokens = controllerIds.stream().map(controllerId -> { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + return target.get().getSecurityToken(); + }).collect(Collectors.toList()); + + final List requestTargets = request.getTargets(); + + assertThat(requestTargets).hasSameSizeAs(controllerIds); + requestTargets.forEach(requestTarget -> { + assertThat(requestTarget).isNotNull(); + assertThat(tokens.contains(requestTarget.getTargetSecurityToken())); + }); + } + + protected void assertEventMessageNotPresent(final EventTopic eventTopic) { + assertThat(replyToListener.getLatestEventMessage(eventTopic)).isNull(); + } + + @Test + @Description("Verify that batch and multi-assignments can't be activated at the same time.") + void assertBatchAndMultiAssignmentsNotCompatible() { + enableBatchAssignments(); + assertThatExceptionOfType(TenantConfigurationValueChangeNotAllowedException.class) + .isThrownBy(() -> enableMultiAssignments()); + disableBatchAssignments(); + + enableMultiAssignments(); + assertThatExceptionOfType(TenantConfigurationValueChangeNotAllowedException.class) + .isThrownBy(() -> enableBatchAssignments()); + } + + @ParameterizedTest + @EnumSource(names = { "BATCH_DOWNLOAD_AND_INSTALL", "BATCH_DOWNLOAD" }) + @Description("Verify payload of batch assignments.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 3), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 3), @Expect(type = ActionUpdatedEvent.class, count = 0), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock + @Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = TargetPollEvent.class, count = 3), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) + void assertBatchAssignmentsMessagePayload(final EventTopic topic) { + enableBatchAssignments(); + + final List targets = Arrays.asList("batchCtrlID1", "batchCtrlID2", "batchCtrlID3"); + for (int i = 0; i < targets.size(); i++) { + registerAndAssertTargetWithExistingTenant(targets.get(i), i + 1); + } + + final DistributionSet ds = testdataFactory.createDistributionSet(); + testdataFactory.addSoftwareModuleMetadata(ds); + + assignDistributionSet(ds.getId(), targets, topic == BATCH_DOWNLOAD ? DOWNLOAD_ONLY : FORCED); + + waitUntilEventMessagesAreDispatchedToTarget(topic); + + final Message message = replyToListener.getLatestEventMessage(topic); + final Map headers = message.getMessageProperties().getHeaders(); + assertThat(headers).containsEntry("type", EVENT.toString()); + assertThat(headers).containsEntry("topic", topic.toString()); + + final DmfBatchDownloadAndUpdateRequest batchRequest = (DmfBatchDownloadAndUpdateRequest) getDmfClient() + .getMessageConverter().fromMessage(message); + + assertThat(batchRequest).isExactlyInstanceOf(DmfBatchDownloadAndUpdateRequest.class); + assertDmfBatchDownloadAndUpdateRequest(batchRequest, ds.getModules(), targets); + } + + @Test + @Description("Verify that a distribution assignment send a confirm message.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) + void sendConfirmStatus() { + final String controllerId = TARGET_PREFIX + "sendConfirmStatus"; + enableConfirmationFlow(); + registerTargetAndAssignDistributionSet(controllerId); + + waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); + assertConfirmMessage(getDistributionSet().getModules(), controllerId); + assertEventMessageNotPresent(EventTopic.DOWNLOAD_AND_INSTALL); + } + + private static Set getSmIds(final DmfDownloadAndUpdateRequest request) { + return request.getSoftwareModules().stream().map(DmfSoftwareModule::getModuleId).collect(Collectors.toSet()); + } + + private static List getDownloadAndUpdateRequests(final DmfMultiActionRequest request) { + return request.getElements().stream() + .filter(AmqpMessageDispatcherServiceIntegrationTest::isDownloadAndUpdateRequest) + .map(multiAction -> (DmfDownloadAndUpdateRequest) multiAction.getAction()).collect(Collectors.toList()); + } + + private static boolean isDownloadAndUpdateRequest(final DmfMultiActionElement multiActionElement) { + return multiActionElement.getTopic().equals(EventTopic.DOWNLOAD) + || multiActionElement.getTopic().equals(EventTopic.DOWNLOAD_AND_INSTALL); + } + + private List getLatestMultiActionMessages(final String expectedControllerId) { + final Message multiactionMessage = replyToListener.getLatestEventMessage(EventTopic.MULTI_ACTION); + assertThat(multiactionMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.THING_ID)) + .isEqualTo(expectedControllerId); + return ((DmfMultiActionRequest) getDmfClient().getMessageConverter().fromMessage(multiactionMessage)) + .getElements(); + } + + private void updateActionViaDmfClient(final String controllerId, final long actionId, + final DmfActionStatus status) { + createAndSendActionStatusUpdateMessage(controllerId, actionId, status); + } + + private Long assignNewDsToTarget(final String controllerId) { + return assignNewDsToTarget(controllerId, null); + } + + private Long assignNewDsToTarget(final String controllerId, final Integer weight) { + final DistributionSet ds = testdataFactory.createDistributionSet(); + final Long actionId = getFirstAssignedActionId( + assignDistributionSet(ds.getId(), Collections.singletonList(controllerId), ActionType.FORCED, + RepositoryModelConstants.NO_FORCE_TIME, weight)); + waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); + return actionId; + } + + private Set getSoftwareModuleIds(final DistributionSet ds) { + return ds.getModules().stream().map(SoftwareModule::getId).collect(Collectors.toSet()); + } + + private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery, final Integer weight) { + final Rollout rollout = testdataFactory.createRolloutByVariables(UUID.randomUUID().toString(), "", 1, + filterQuery, ds, "50", "5", ActionType.FORCED, weight, false); + rolloutManagement.start(rollout.getId()); + rolloutHandler.handleAll(); + return rollout; + } + private void waitUntilTargetHasStatus(final String controllerId, final TargetUpdateStatus status) { waitUntil(() -> { final Optional findTargetByControllerID = targetManagement.getByControllerID(controllerId); @@ -614,129 +739,4 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer .map(request -> new SimpleEntry<>(request.getAction().getActionId(), request.getTopic())) .collect(Collectors.toList()); } - - private static Set getSmIds(final DmfDownloadAndUpdateRequest request) { - return request.getSoftwareModules().stream().map(DmfSoftwareModule::getModuleId).collect(Collectors.toSet()); - } - - private static List getDownloadAndUpdateRequests(final DmfMultiActionRequest request) { - return request.getElements().stream() - .filter(AmqpMessageDispatcherServiceIntegrationTest::isDownloadAndUpdateRequest) - .map(multiAction -> (DmfDownloadAndUpdateRequest) multiAction.getAction()).collect(Collectors.toList()); - } - - private static boolean isDownloadAndUpdateRequest(final DmfMultiActionElement multiActionElement) { - return multiActionElement.getTopic().equals(EventTopic.DOWNLOAD) - || multiActionElement.getTopic().equals(EventTopic.DOWNLOAD_AND_INSTALL); - } - - @Test - @Description("Verify payload of batch assignment download and install message.") - public void assertBatchAssignmentsDownloadAndInstall() { - assertBatchAssignmentsMessagePayload(BATCH_DOWNLOAD_AND_INSTALL); - } - - @Test - @Description("Verify payload of batch assignments download only message.") - public void assertBatchAssignmentsDownloadOnly() { - assertBatchAssignmentsMessagePayload(BATCH_DOWNLOAD); - } - - @Test - @Description("Verify that batch and multi-assignments can't be activated at the same time.") - void assertBatchAndMultiAssignmentsNotCompatible() { - enableBatchAssignments(); - assertThatExceptionOfType(TenantConfigurationValueChangeNotAllowedException.class) - .isThrownBy(() -> enableMultiAssignments()); - disableBatchAssignments(); - - enableMultiAssignments(); - assertThatExceptionOfType(TenantConfigurationValueChangeNotAllowedException.class) - .isThrownBy(() -> enableBatchAssignments()); - } - - @ParameterizedTest - @EnumSource(names = { "BATCH_DOWNLOAD_AND_INSTALL", "BATCH_DOWNLOAD" }) - @Description("Verify payload of batch assignments.") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 3), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionCreatedEvent.class, count = 3), @Expect(type = ActionUpdatedEvent.class, count = 0), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock - @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock - @Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = TargetPollEvent.class, count = 3), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) - void assertBatchAssignmentsMessagePayload(final EventTopic topic) { - enableBatchAssignments(); - - final List targets = Arrays.asList("batchCtrlID1", "batchCtrlID2", "batchCtrlID3"); - for (int i = 0; i < targets.size(); i++) { - registerAndAssertTargetWithExistingTenant(targets.get(i), i+1); - } - - final DistributionSet ds = testdataFactory.createDistributionSet(); - testdataFactory.addSoftwareModuleMetadata(ds); - - assignDistributionSet(ds.getId(), targets, topic == BATCH_DOWNLOAD?DOWNLOAD_ONLY:FORCED); - - waitUntilEventMessagesAreDispatchedToTarget(topic); - - final Message message = replyToListener.getLatestEventMessage(topic); - final Map headers = message.getMessageProperties().getHeaders(); - assertThat(headers).containsEntry("type", EVENT.toString()); - assertThat(headers).containsEntry("topic",topic.toString()); - - final DmfBatchDownloadAndUpdateRequest batchRequest = (DmfBatchDownloadAndUpdateRequest) getDmfClient() - .getMessageConverter().fromMessage(message); - - assertThat(batchRequest).isExactlyInstanceOf(DmfBatchDownloadAndUpdateRequest.class); - assertDmfBatchDownloadAndUpdateRequest(batchRequest, ds.getModules(), targets); - } - - protected void assertDmfBatchDownloadAndUpdateRequest(final DmfBatchDownloadAndUpdateRequest request, - final Set softwareModules, - final List controllerIds) { - assertSoftwareModules(softwareModules, request.getSoftwareModules()); - - final List tokens = controllerIds.stream().map(controllerId -> { - final Optional target = controllerManagement.getByControllerId(controllerId); - assertThat(target).isPresent(); - return target.get().getSecurityToken(); - }).collect(Collectors.toList()); - - - final List requestTargets = request.getTargets(); - - assertThat(requestTargets).hasSameSizeAs(controllerIds); - requestTargets.forEach(requestTarget -> { - assertThat(requestTarget).isNotNull(); - assertThat(tokens.contains(requestTarget.getTargetSecurityToken())); - }); - } - - @Test - @Description("Verify that a distribution assignment send a confirm message.") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionCreatedEvent.class, count = 1), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock - @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock - @Expect(type = TargetUpdatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) - void sendConfirmStatus() { - final String controllerId = TARGET_PREFIX + "sendConfirmStatus"; - enableConfirmationFlow(); - registerTargetAndAssignDistributionSet(controllerId); - - waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING); - assertConfirmMessage(getDistributionSet().getModules(), controllerId); - assertEventMessageNotPresent(EventTopic.DOWNLOAD_AND_INSTALL); - } - protected void assertEventMessageNotPresent(final EventTopic eventTopic) { - assertThat(replyToListener.getLatestEventMessage(eventTopic)).isNull(); - } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java index f626d3f50..0e917ab93 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java @@ -26,6 +26,12 @@ import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Step; +import io.qameta.allure.Story; import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.amqp.AmqpMessageHandlerService; import org.eclipse.hawkbit.amqp.AmqpProperties; @@ -37,12 +43,12 @@ import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate; import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent; -import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; @@ -63,8 +69,8 @@ import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; -import org.eclipse.hawkbit.repository.test.util.TargetTestData; import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch; +import org.eclipse.hawkbit.repository.test.util.TargetTestData; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullSource; @@ -74,22 +80,14 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.beans.factory.annotation.Autowired; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import io.qameta.allure.Description; -import io.qameta.allure.Feature; -import io.qameta.allure.Step; -import io.qameta.allure.Story; - @Feature("Component Tests - Device Management Federation API") @Story("Amqp Message Handler Service") class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegrationTest { - private static final String TARGET_PREFIX = "Dmf_hand_"; + public static final String DMF_REGISTER_TEST_CONTROLLER_ID = "Dmf_hand_registerTargets_1"; public static final String DMF_ATTR_TEST_CONTROLLER_ID = "Dmf_hand_updateAttributes"; public static final String UPDATE_ATTR_TEST_CONTROLLER_ID = "ControllerAttributeTestTarget"; - + private static final String TARGET_PREFIX = "Dmf_hand_"; @Autowired private AmqpProperties amqpProperties; @@ -179,7 +177,7 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr } @ParameterizedTest - @ValueSource(strings = {"", "Invalid Invalid"}) + @ValueSource(strings = { "", "Invalid Invalid" }) @NullSource @Description("Tests register invalid target with empty controller id.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class) }) @@ -309,7 +307,7 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr } @ParameterizedTest - @ValueSource(strings={"", "NotExist"}) + @ValueSource(strings = { "", "NotExist" }) @NullSource @Description("Tests null type message header. This message should forwarded to the deadletter queue") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class) }) @@ -323,7 +321,7 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr } @ParameterizedTest - @ValueSource(strings = {"", "NotExist"}) + @ValueSource(strings = { "", "NotExist" }) @NullSource @Description("Tests null topic message header. This message should forwarded to the deadletter queue") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class) }) @@ -347,7 +345,7 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr } @ParameterizedTest - @ValueSource(strings = { "", "Invalid Content"}) + @ValueSource(strings = { "", "Invalid Content" }) @NullSource @Description("Tests invalid null message content. This message should forwarded to the deadletter queue") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class) }) @@ -688,84 +686,6 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr } - @Step - private void updateAttributesWithUpdateModeRemove() { - - // assemble the expected attributes - final Map expectedAttributes = targetManagement - .getControllerAttributes(DMF_ATTR_TEST_CONTROLLER_ID); - expectedAttributes.remove("k1"); - expectedAttributes.remove("k3"); - - // send an update message with update mode - final Map removeAttributes = new HashMap<>(); - removeAttributes.put("k1", "foo"); - removeAttributes.put("k3", "bar"); - - final DmfAttributeUpdate remove = new DmfAttributeUpdate(); - remove.setMode(DmfUpdateMode.REMOVE); - remove.getAttributes().putAll(removeAttributes); - sendUpdateAttributeMessage(remove); - - // validate - assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); - } - - @Step - private void updateAttributesWithUpdateModeMerge() { - // get the current attributes - final Map attributes = new HashMap<>( - targetManagement.getControllerAttributes(DMF_ATTR_TEST_CONTROLLER_ID)); - - // send an update message with update mode MERGE - final Map mergeAttributes = new HashMap<>(); - mergeAttributes.put("k1", "v1_modified_again"); - mergeAttributes.put("k4", "v4"); - - final DmfAttributeUpdate merge = new DmfAttributeUpdate(); - merge.setMode(DmfUpdateMode.MERGE); - merge.getAttributes().putAll(mergeAttributes); - sendUpdateAttributeMessage(merge); - - // validate - final Map expectedAttributes = new HashMap<>(); - expectedAttributes.putAll(attributes); - expectedAttributes.putAll(mergeAttributes); - assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); - } - - @Step - private void updateAttributesWithUpdateModeReplace() { - // send an update message with update mode REPLACE - final Map expectedAttributes = new HashMap<>(); - expectedAttributes.put("k1", "v1_modified"); - expectedAttributes.put("k2", "v2"); - expectedAttributes.put("k3", "v3"); - - final DmfAttributeUpdate replace = new DmfAttributeUpdate(); - replace.setMode(DmfUpdateMode.REPLACE); - replace.getAttributes().putAll(expectedAttributes); - sendUpdateAttributeMessage(replace); - - // validate - assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); - } - - @Step - private void updateAttributesWithoutUpdateMode() { - // send an update message which does not specify an update mode - final Map expectedAttributes = new HashMap<>(); - expectedAttributes.put("k0", "v0"); - expectedAttributes.put("k1", "v1"); - - final DmfAttributeUpdate defaultUpdate = new DmfAttributeUpdate(); - defaultUpdate.getAttributes().putAll(expectedAttributes); - sendUpdateAttributeMessage(defaultUpdate); - - // validate - assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); - } - @Test @Description("Verify that sending an update controller attribute message with no thingid header to an existing target does not work.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @@ -917,6 +837,234 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr } } + @Test + @Description("Register a target and send a update action status (confirmed). Verify if the updated action status is correct.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) + void confirmedActionStatus() { + enableConfirmationFlow(); + final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; + + final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); + final Long actionId = getFirstAssignedActionId(assignmentResult); + createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.CONFIRMED); + assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION, Status.RUNNING); + + // assert download and install message + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.CONFIRM, EventTopic.DOWNLOAD_AND_INSTALL); + assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId); + } + + @Test + @Description("Verify the DMF confirmed feedback can be provided if confirmation flow is disabled") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), + @Expect(type = TenantConfigurationUpdatedEvent.class, count = 1) }) + void verifyActionCanBeConfirmedOnDisabledConfirmationFlow() { + enableConfirmationFlow(); + final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; + + final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); + final Long actionId = getFirstAssignedActionId(assignmentResult); + // verify action status is in WAIT_FOR_CONFIRMATION + assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); + + disableConfirmationFlow(); + + createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.CONFIRMED); + assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION, Status.RUNNING); + + // assert download and install message + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.CONFIRM, EventTopic.DOWNLOAD_AND_INSTALL); + assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId); + } + + @Test + @Description("Verify the DMF confirmed feedback can be provided if confirmation flow is disabled") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 0), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), + @Expect(type = TenantConfigurationUpdatedEvent.class, count = 1) }) + void verifyActionCanBeDeniedOnDisabledConfirmationFlow() { + enableConfirmationFlow(); + final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; + + final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); + final Long actionId = getFirstAssignedActionId(assignmentResult); + // verify action status is in WAIT_FOR_CONFIRMATION + assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); + + disableConfirmationFlow(); + + createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DENIED); + assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION); + } + + @Test + @Description("Verify the DMF download and install message is send directly if auto-confirmation is active") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 0), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) + void verifyDownloadAndInstallDirectlySendOnAutoConfirmationEnabled() { + enableConfirmationFlow(); + final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; + + registerAndAssertTargetWithExistingTenant(controllerId); + + confirmationManagement.activateAutoConfirmation(controllerId, null, null); + + final DistributionSetAssignmentResult assignmentResult = prepareDistributionSetAndAssign(controllerId); + final Long actionId = getFirstAssignedActionId(assignmentResult); + // verify action status is in WAIT_FOR_CONFIRMATION + assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); + + // assert download and install message + waitUntilEventMessagesAreDispatchedToTarget(EventTopic.DOWNLOAD_AND_INSTALL); + assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId); + } + + @Test + @Description("Register a target and send a update action status (denied). Verify if the updated action status is correct.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class), @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) + void deniedActionStatus() { + enableConfirmationFlow(); + final String controllerId = TARGET_PREFIX + "deniedActionStatus"; + final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); + final Long actionId = getFirstAssignedActionId(assignmentResult); + assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); + createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DENIED); + assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION); + } + + private static String getActionIdFromBody(final byte[] body) throws IOException { + final ObjectMapper objectMapper = new ObjectMapper(); + final ObjectNode node = objectMapper.readValue(new String(body, Charset.defaultCharset()), ObjectNode.class); + assertThat(node.has("actionId")).isTrue(); + return node.get("actionId").asText(); + } + + @Step + private void updateAttributesWithUpdateModeRemove() { + + // assemble the expected attributes + final Map expectedAttributes = targetManagement + .getControllerAttributes(DMF_ATTR_TEST_CONTROLLER_ID); + expectedAttributes.remove("k1"); + expectedAttributes.remove("k3"); + + // send an update message with update mode + final Map removeAttributes = new HashMap<>(); + removeAttributes.put("k1", "foo"); + removeAttributes.put("k3", "bar"); + + final DmfAttributeUpdate remove = new DmfAttributeUpdate(); + remove.setMode(DmfUpdateMode.REMOVE); + remove.getAttributes().putAll(removeAttributes); + sendUpdateAttributeMessage(remove); + + // validate + assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); + } + + @Step + private void updateAttributesWithUpdateModeMerge() { + // get the current attributes + final Map attributes = new HashMap<>( + targetManagement.getControllerAttributes(DMF_ATTR_TEST_CONTROLLER_ID)); + + // send an update message with update mode MERGE + final Map mergeAttributes = new HashMap<>(); + mergeAttributes.put("k1", "v1_modified_again"); + mergeAttributes.put("k4", "v4"); + + final DmfAttributeUpdate merge = new DmfAttributeUpdate(); + merge.setMode(DmfUpdateMode.MERGE); + merge.getAttributes().putAll(mergeAttributes); + sendUpdateAttributeMessage(merge); + + // validate + final Map expectedAttributes = new HashMap<>(); + expectedAttributes.putAll(attributes); + expectedAttributes.putAll(mergeAttributes); + assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); + } + + @Step + private void updateAttributesWithUpdateModeReplace() { + // send an update message with update mode REPLACE + final Map expectedAttributes = new HashMap<>(); + expectedAttributes.put("k1", "v1_modified"); + expectedAttributes.put("k2", "v2"); + expectedAttributes.put("k3", "v3"); + + final DmfAttributeUpdate replace = new DmfAttributeUpdate(); + replace.setMode(DmfUpdateMode.REPLACE); + replace.getAttributes().putAll(expectedAttributes); + sendUpdateAttributeMessage(replace); + + // validate + assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); + } + + @Step + private void updateAttributesWithoutUpdateMode() { + // send an update message which does not specify an update mode + final Map expectedAttributes = new HashMap<>(); + expectedAttributes.put("k0", "v0"); + expectedAttributes.put("k1", "v1"); + + final DmfAttributeUpdate defaultUpdate = new DmfAttributeUpdate(); + defaultUpdate.getAttributes().putAll(expectedAttributes); + sendUpdateAttributeMessage(defaultUpdate); + + // validate + assertUpdateAttributes(DMF_ATTR_TEST_CONTROLLER_ID, expectedAttributes); + } + @Step private void verifyAssignedDsAndInstalledDs(final Long assignedDsId, final Long installedDsId) { final Optional target = controllerManagement.getByControllerId(DMF_REGISTER_TEST_CONTROLLER_ID); @@ -1014,6 +1162,7 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr } }); } + private int getAuthenticationMessageCount() { return Integer .parseInt(Objects.requireNonNull(getRabbitAdmin().getQueueProperties(amqpProperties.getReceiverQueue())) @@ -1034,155 +1183,4 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr .verify(getDeadletterListener(), Mockito.times(numberOfInvocations)).handleMessage(Mockito.any())); Mockito.reset(getDeadletterListener()); } - - private static String getActionIdFromBody(final byte[] body) throws IOException { - final ObjectMapper objectMapper = new ObjectMapper(); - final ObjectNode node = objectMapper.readValue(new String(body, Charset.defaultCharset()), ObjectNode.class); - assertThat(node.has("actionId")).isTrue(); - return node.get("actionId").asText(); - } - - @Test - @Description("Register a target and send a update action status (confirmed). Verify if the updated action status is correct.") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionUpdatedEvent.class, count = 1), - @Expect(type = ActionCreatedEvent.class, count = 1), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock - @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock - @Expect(type = TargetUpdatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) - void confirmedActionStatus() { - enableConfirmationFlow(); - final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; - - final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); - final Long actionId = getFirstAssignedActionId(assignmentResult); - createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.CONFIRMED); - assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION, Status.RUNNING); - - // assert download and install message - waitUntilEventMessagesAreDispatchedToTarget(EventTopic.CONFIRM, EventTopic.DOWNLOAD_AND_INSTALL); - assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId); - } - - @Test - @Description("Verify the DMF confirmed feedback can be provided if confirmation flow is disabled") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionUpdatedEvent.class, count = 1), - @Expect(type = ActionCreatedEvent.class, count = 1), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock - @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock - @Expect(type = TargetUpdatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), - @Expect(type = TenantConfigurationUpdatedEvent.class, count = 1) }) - void verifyActionCanBeConfirmedOnDisabledConfirmationFlow() { - enableConfirmationFlow(); - final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; - - final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); - final Long actionId = getFirstAssignedActionId(assignmentResult); - // verify action status is in WAIT_FOR_CONFIRMATION - assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); - - disableConfirmationFlow(); - - createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.CONFIRMED); - assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION, Status.RUNNING); - - // assert download and install message - waitUntilEventMessagesAreDispatchedToTarget(EventTopic.CONFIRM, EventTopic.DOWNLOAD_AND_INSTALL); - assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId); - } - - - @Test - @Description("Verify the DMF confirmed feedback can be provided if confirmation flow is disabled") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionUpdatedEvent.class, count = 0), - @Expect(type = ActionCreatedEvent.class, count = 1), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock - @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock - @Expect(type = TargetUpdatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), - @Expect(type = TenantConfigurationUpdatedEvent.class, count = 1)}) - void verifyActionCanBeDeniedOnDisabledConfirmationFlow() { - enableConfirmationFlow(); - final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; - - final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); - final Long actionId = getFirstAssignedActionId(assignmentResult); - // verify action status is in WAIT_FOR_CONFIRMATION - assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); - - disableConfirmationFlow(); - - createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DENIED); - assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION); - } - - @Test - @Description("Verify the DMF download and install message is send directly if auto-confirmation is active") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionUpdatedEvent.class, count = 0), - @Expect(type = ActionCreatedEvent.class, count = 1), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock - @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = TargetUpdatedEvent.class, count = 2), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) - void verifyDownloadAndInstallDirectlySendOnAutoConfirmationEnabled() { - enableConfirmationFlow(); - final String controllerId = TARGET_PREFIX + "confirmedActionStatus"; - - registerAndAssertTargetWithExistingTenant(controllerId); - - confirmationManagement.activateAutoConfirmation(controllerId, null, null); - - final DistributionSetAssignmentResult assignmentResult = prepareDistributionSetAndAssign(controllerId); - final Long actionId = getFirstAssignedActionId(assignmentResult); - // verify action status is in WAIT_FOR_CONFIRMATION - assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); - - // assert download and install message - waitUntilEventMessagesAreDispatchedToTarget(EventTopic.DOWNLOAD_AND_INSTALL); - assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId); - } - - @Test - @Description("Register a target and send a update action status (denied). Verify if the updated action status is correct.") - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionUpdatedEvent.class), @Expect(type = ActionCreatedEvent.class, count = 1), - @Expect(type = DistributionSetCreatedEvent.class, count = 1), - @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), - @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock - @Expect(type = SoftwareModuleUpdatedEvent.class, count = 9), // implicit lock - @Expect(type = TargetUpdatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) - void deniedActionStatus() { - enableConfirmationFlow(); - final String controllerId = TARGET_PREFIX + "deniedActionStatus"; - final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId); - final Long actionId = getFirstAssignedActionId(assignmentResult); - assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION); - createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DENIED); - assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION); - } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java index 9dc5eec4f..e95539379 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java @@ -33,16 +33,15 @@ public final class SoftwareModuleJsonMatcher { * null *

* For example: - * + * *

      * List modules;
      * List expectedModules;
-     * 
+     *
      * assertThat(modules, containsExactly(expectedModules));
      * 
- * - * @param expectedModules - * the json sofware modules. + * + * @param expectedModules the json sofware modules. */ public static SoftwareModulesMatcher containsExactly(final List expectedModules) { return new SoftwareModulesMatcher(expectedModules); @@ -56,13 +55,22 @@ public final class SoftwareModuleJsonMatcher { this.expectedModules = expectedModules; } + @Override + public boolean matches(final Object actualValue) { + return containsExactly(actualValue); + } + + @Override + public void describeTo(final Description description) { + description.appendValue(expectedModules); + } + boolean containsExactly(final Object actual) { if (actual == null) { return expectedModules == null; } - @SuppressWarnings("unchecked") - final Collection modules = (Collection) actual; + @SuppressWarnings("unchecked") final Collection modules = (Collection) actual; return expectedModules.stream().allMatch(e -> existsIn(e, modules)); } @@ -73,16 +81,6 @@ public final class SoftwareModuleJsonMatcher { && module.getModuleVersion().equals(e.getVersion()) && module.getArtifacts().size() == e.getArtifacts().size()); } - - @Override - public boolean matches(final Object actualValue) { - return containsExactly(actualValue); - } - - @Override - public void describeTo(final Description description) { - description.appendValue(expectedModules); - } } }