From 98535c96b4825b2717e5cfce29688da304d0838b Mon Sep 17 00:00:00 2001 From: Bondar Bogdan <36962546+bogdan-bondar@users.noreply.github.com> Date: Mon, 2 Aug 2021 17:45:48 +0200 Subject: [PATCH] Optional controller attributes for THING_CREATED (#1154) * added optional attributes payload to THING_CREATED DMF message Signed-off-by: Bogdan Bondar * added optional attributes unit test for THING_CREATED message handler Signed-off-by: Bogdan Bondar * extended Target interface with getControllerAttributes method, extended DMF message handler integration tests Signed-off-by: Bogdan Bondar * updated hawkbit docs Signed-off-by: Bogdan Bondar * fixed review findings Signed-off-by: Bogdan Bondar --- docs/content/apis/dmf_api.md | 14 +- .../amqp/AmqpMessageHandlerService.java | 26 ++- .../amqp/AmqpMessageHandlerServiceTest.java | 188 ++++++++++++------ .../AbstractAmqpServiceIntegrationTest.java | 98 ++++++--- ...pMessageHandlerServiceIntegrationTest.java | 88 ++++++-- .../dmf/json/model/DmfCreateThing.java | 10 + .../hawkbit/repository/model/Target.java | 1 - 7 files changed, 307 insertions(+), 118 deletions(-) diff --git a/docs/content/apis/dmf_api.md b/docs/content/apis/dmf_api.md index 29662c6bc..f4bd51142 100644 --- a/docs/content/apis/dmf_api.md +++ b/docs/content/apis/dmf_api.md @@ -63,11 +63,19 @@ Payload Template (optional): ```json { - "name": "String" + "name": "String", + "attributeUpdate": { + "attributes": { + "exampleKey1" : "exampleValue1", + "exampleKey2" : "exampleValue2" + }, + "mode": "String" + } } ``` -The "name" property specifies the name of the thing, which by default is the thing ID. This property is optional. +The "name" property specifies the name of the thing, which by default is the thing ID. This property is optional.
+The "attributeUpdate" property provides the attributes of the thing, for details see UPDATE_ATTRIBUTES message. This property is optional. ### THING_REMOVED @@ -93,7 +101,7 @@ type=THING\_REMOVED
tenant=default
thingId=abc | content\_type=app ### UPDATE_ATTRIBUTES -Message to update target attributes. This message can be send in response to a _REQUEST_ATTRIBUTES_UPDATE_ event, sent by hawkBit. +Message to update target attributes. This message can be send in response to a REQUEST_ATTRIBUTES_UPDATE event, sent by hawkBit. | Header | Description | Type | Mandatory |-----------------------------|----------------------------------|-------------------------------------|---------------- 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 6eca6ca92..fdeb16205 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 @@ -10,7 +10,6 @@ package org.eclipse.hawkbit.amqp; import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; -import static org.springframework.util.StringUtils.hasText; import java.io.Serializable; import java.net.URI; @@ -53,7 +52,6 @@ import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.messaging.handler.annotation.Header; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -201,12 +199,12 @@ public class AmqpMessageHandlerService extends BaseAmqpService { } /** - * 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. + * 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 - * in body + * the message that contains replyTo property and optionally the + * name and attributes in body * @param virtualHost * the virtual host */ @@ -225,14 +223,22 @@ public class AmqpMessageHandlerService extends BaseAmqpService { target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri); } else { checkContentTypeJson(message); + final DmfCreateThing thingCreateBody = convertMessage(message, DmfCreateThing.class); + final DmfAttributeUpdate thingAttributeUpdateBody = thingCreateBody.getAttributeUpdate(); - target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri, convertMessage(message, DmfCreateThing.class).getName()); + target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri, + thingCreateBody.getName()); + if (thingAttributeUpdateBody != null) { + controllerManagement.updateControllerAttributes(thingId, thingAttributeUpdateBody.getAttributes(), + getUpdateMode(thingAttributeUpdateBody)); + } } LOG.debug("Target {} reported online state.", thingId); sendUpdateCommandToTarget(target); } catch (final EntityAlreadyExistsException e) { - throw new AmqpRejectAndDontRequeueException("Tried to register previously registered target, message will be ignored!", e); + throw new AmqpRejectAndDontRequeueException( + "Tried to register previously registered target, message will be ignored!", e); } } @@ -251,8 +257,8 @@ public class AmqpMessageHandlerService extends BaseAmqpService { } private void sendCurrentActionsAsMultiActionToTarget(final Target target) { - final List actions = controllerManagement - .findActiveActionsWithHighestWeight(target.getControllerId(), MAX_ACTION_COUNT); + final List actions = controllerManagement.findActiveActionsWithHighestWeight(target.getControllerId(), + MAX_ACTION_COUNT); final Set distributionSets = actions.stream().map(Action::getDistributionSet) .collect(Collectors.toSet()); 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 b3ec8f82d..0688f685f 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 @@ -14,8 +14,8 @@ import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationPrope import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -79,6 +79,7 @@ import org.springframework.http.HttpStatus; import io.qameta.allure.Description; import io.qameta.allure.Feature; +import io.qameta.allure.Step; import io.qameta.allure.Story; @ExtendWith(MockitoExtension.class) @@ -143,6 +144,9 @@ public class AmqpMessageHandlerServiceTest { @Captor private ArgumentCaptor targetIdCaptor; + @Captor + private ArgumentCaptor targetNameCaptor; + @Captor private ArgumentCaptor uriCaptor; @@ -185,50 +189,64 @@ public class AmqpMessageHandlerServiceTest { @Description("Tests the creation of a target/thing by calling the same method that incoming RabbitMQ messages would access.") public void createThing() { final String knownThingId = "1"; - final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); - messageProperties.setHeader(MessageHeaderKey.THING_ID, "1"); - final Message message = messageConverter.toMessage(new byte[0], messageProperties); + + processThingCreatedMessage(knownThingId, null); + + assertThingIdCapturedField(knownThingId); + 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); - - final ArgumentCaptor targetIdCaptor = ArgumentCaptor.forClass(String.class); - final ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); - when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), - uriCaptor.capture())).thenReturn(targetMock); + if (payload == null) { + when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), + uriCaptor.capture())).thenReturn(targetMock); + } else { + when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), + uriCaptor.capture(), targetNameCaptor.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); + } - // verify - assertThat(targetIdCaptor.getValue()).as("Thing id is wrong").isEqualTo(knownThingId); - assertThat(uriCaptor.getValue().toString()).as("Uri is not right") - .isEqualTo("amqp://" + VIRTUAL_HOST + "/MyTest"); + @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); } @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() { final String knownThingId = "2"; - final DmfCreateThing targetProperties = new DmfCreateThing(); - targetProperties.setName("NonDefaultTargetName"); + final String knownThingName = "NonDefaultTargetName"; - final Target targetMock = mock(Target.class); + final DmfCreateThing payload = new DmfCreateThing(); + payload.setName(knownThingName); - targetIdCaptor = ArgumentCaptor.forClass(String.class); - uriCaptor = ArgumentCaptor.forClass(URI.class); - final ArgumentCaptor targetNameCaptor = ArgumentCaptor.forClass(String.class); + processThingCreatedMessage(knownThingId, payload); - when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), - uriCaptor.capture(), targetNameCaptor.capture())).thenReturn(targetMock); - when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.empty()); + assertThingIdCapturedField(knownThingId); + assertReplyToCapturedField("MyTest"); + assertThingNameCapturedField(knownThingName); + } - amqpMessageHandlerService.onMessage( - createMessage(targetProperties, getThingCreatedMessageProperties(knownThingId)), - MessageType.THING_CREATED.name(), TENANT, "vHost"); - - assertThat(targetIdCaptor.getValue()).as("Thing id is wrong").isEqualTo(knownThingId); - assertThat(uriCaptor.getValue().toString()).as("Uri is not right").isEqualTo("amqp://vHost/MyTest"); - assertThat(targetNameCaptor.getValue()).as("Thing name is not right").isEqualTo(targetProperties.getName()); + @Step + private void assertThingNameCapturedField(final String thingName) { + assertThat(targetNameCaptor.getValue()).as("Thing name is wrong").isEqualTo(thingName); } @Test @@ -243,6 +261,58 @@ public class AmqpMessageHandlerServiceTest { MessageType.THING_CREATED.name(), TENANT, VIRTUAL_HOST)); } + @Test + @Description("Tests the creation of a target/thing with specified attributes by calling the same method that incoming RabbitMQ messages would access.") + public void createThingWithAttributes() { + final String knownThingId = "4"; + + final DmfAttributeUpdate attributeUpdate = new DmfAttributeUpdate(); + attributeUpdate.getAttributes().put("testKey1", "testValue1"); + attributeUpdate.getAttributes().put("testKey2", "testValue2"); + + final DmfCreateThing payload = new DmfCreateThing(); + payload.setAttributeUpdate(attributeUpdate); + + processThingCreatedMessage(knownThingId, payload); + + assertThingIdCapturedField(knownThingId); + assertReplyToCapturedField("MyTest"); + 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() { + final String knownThingId = "5"; + final String knownThingName = "NonDefaultTargetName"; + + final DmfAttributeUpdate attributeUpdate = new DmfAttributeUpdate(); + attributeUpdate.getAttributes().put("testKey1", "testValue1"); + attributeUpdate.getAttributes().put("testKey2", "testValue2"); + attributeUpdate.setMode(DmfUpdateMode.REPLACE); + + final DmfCreateThing payload = new DmfCreateThing(); + payload.setName(knownThingName); + payload.setAttributeUpdate(attributeUpdate); + + processThingCreatedMessage(knownThingId, payload); + + assertThingIdCapturedField(knownThingId); + assertReplyToCapturedField("MyTest"); + assertThingAttributesCapturedField(attributeUpdate.getAttributes()); + 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() { @@ -254,18 +324,15 @@ public class AmqpMessageHandlerServiceTest { attributeUpdate.getAttributes().put("testKey1", "testValue1"); attributeUpdate.getAttributes().put("testKey2", "testValue2"); - final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, - messageProperties); + final Message message = createMessage(attributeUpdate, messageProperties); when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), attributesCaptor.capture(), modeCaptor.capture())).thenReturn(null); amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); - // verify - assertThat(targetIdCaptor.getValue()).as("Thing id is wrong").isEqualTo(knownThingId); - assertThat(attributesCaptor.getValue()).as("Attributes is not right") - .isEqualTo(attributeUpdate.getAttributes()); + assertThingIdCapturedField(knownThingId); + assertThingAttributesCapturedField(attributeUpdate.getAttributes()); } @Test @@ -283,32 +350,32 @@ public class AmqpMessageHandlerServiceTest { modeCaptor.capture())).thenReturn(null); // send a message which does not specify a update mode - Message message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + Message message = createMessage(attributeUpdate, messageProperties); amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); // verify that NO fallback is made on the way to the controller // management layer - assertThat(modeCaptor.getValue()).isNull(); + assertThingAttributesModeCapturedField(null); // send a message which specifies update mode MERGE attributeUpdate.setMode(DmfUpdateMode.MERGE); - message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + message = createMessage(attributeUpdate, messageProperties); amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); // verify that the update mode is converted and forwarded as expected - assertThat(modeCaptor.getValue()).isEqualTo(UpdateMode.MERGE); + assertThingAttributesModeCapturedField(UpdateMode.MERGE); // send a message which specifies update mode REPLACE attributeUpdate.setMode(DmfUpdateMode.REPLACE); - message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + message = createMessage(attributeUpdate, messageProperties); amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); // verify that the update mode is converted and forwarded as expected - assertThat(modeCaptor.getValue()).isEqualTo(UpdateMode.REPLACE); + assertThingAttributesModeCapturedField(UpdateMode.REPLACE); // send a message which specifies update mode REMOVE attributeUpdate.setMode(DmfUpdateMode.REMOVE); - message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + message = createMessage(attributeUpdate, messageProperties); amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); // verify that the update mode is converted and forwarded as expected - assertThat(modeCaptor.getValue()).isEqualTo(UpdateMode.REMOVE); + assertThingAttributesModeCapturedField(UpdateMode.REMOVE); } @Test @@ -316,7 +383,7 @@ public class AmqpMessageHandlerServiceTest { public void createThingWithoutReplyTo() { final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED, null); messageProperties.setHeader(MessageHeaderKey.THING_ID, "1"); - final Message message = messageConverter.toMessage("", messageProperties); + final Message message = createMessage("", messageProperties); assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no replyTo header was set") @@ -328,7 +395,7 @@ public class AmqpMessageHandlerServiceTest { @Description("Tests the creation of a target/thing without a thingID by calling the same method that incoming RabbitMQ messages would access.") public void createThingWithoutID() { final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); - final Message message = messageConverter.toMessage(new byte[0], messageProperties); + final Message message = createMessage(new byte[0], messageProperties); assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no thingId was set") @@ -342,7 +409,7 @@ public class AmqpMessageHandlerServiceTest { final String type = "bumlux"; final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); messageProperties.setHeader(MessageHeaderKey.THING_ID, ""); - final Message message = messageConverter.toMessage(new byte[0], messageProperties); + final Message message = createMessage(new byte[0], messageProperties); assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "due to unknown message type") @@ -378,8 +445,7 @@ public class AmqpMessageHandlerServiceTest { final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); final DmfActionUpdateStatus actionUpdateStatus = new DmfActionUpdateStatus(1L, DmfActionStatus.DOWNLOAD); - final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, - messageProperties); + final Message message = createMessage(actionUpdateStatus, messageProperties); assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no action id was set") @@ -395,8 +461,7 @@ public class AmqpMessageHandlerServiceTest { when(controllerManagementMock.findActionWithDetails(anyLong())).thenReturn(Optional.empty()); final DmfActionUpdateStatus actionUpdateStatus = createActionUpdateStatus(DmfActionStatus.DOWNLOAD); - final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, - messageProperties); + final Message message = createMessage(actionUpdateStatus, messageProperties); assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no action id was set") @@ -410,8 +475,7 @@ public class AmqpMessageHandlerServiceTest { final MessageProperties messageProperties = createMessageProperties(null); final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1("12345")); - final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, - messageProperties); + final Message message = createMessage(securityToken, messageProperties); // test final Message onMessage = amqpAuthenticationMessageHandlerService.onAuthenticationRequest(message); @@ -429,8 +493,7 @@ public class AmqpMessageHandlerServiceTest { final MessageProperties messageProperties = createMessageProperties(null); final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1("12345")); - final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, - messageProperties); + final Message message = createMessage(securityToken, messageProperties); final Artifact localArtifactMock = mock(Artifact.class); when(artifactManagementMock.findFirstBySHA1(anyString())).thenReturn(Optional.of(localArtifactMock)); @@ -451,8 +514,7 @@ public class AmqpMessageHandlerServiceTest { final MessageProperties messageProperties = createMessageProperties(null); final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1("12345")); - final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, - messageProperties); + final Message message = createMessage(securityToken, messageProperties); // mock final Artifact localArtifactMock = mock(Artifact.class); @@ -494,14 +556,12 @@ public class AmqpMessageHandlerServiceTest { when(create.status(any())).thenReturn(create); when(entityFactoryMock.actionStatus()).thenReturn(builder); // for the test the same action can be used - when(controllerManagementMock.findActiveActionWithHighestWeight(any())) - .thenReturn(Optional.of(action)); + when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.of(action)); final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); final DmfActionUpdateStatus actionUpdateStatus = createActionUpdateStatus(DmfActionStatus.FINISHED, 23L); - final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, - messageProperties); + final Message message = createMessage(actionUpdateStatus, messageProperties); // test amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); @@ -579,7 +639,7 @@ public class AmqpMessageHandlerServiceTest { final String knownThingId = "1"; final MessageProperties messageProperties = createMessageProperties(MessageType.THING_REMOVED); messageProperties.setHeader(MessageHeaderKey.THING_ID, knownThingId); - final Message message = messageConverter.toMessage(new byte[0], messageProperties); + final Message message = createMessage(new byte[0], messageProperties); // test amqpMessageHandlerService.onMessage(message, MessageType.THING_REMOVED.name(), TENANT, VIRTUAL_HOST); @@ -593,7 +653,7 @@ public class AmqpMessageHandlerServiceTest { public void deleteThingWithoutThingId() { // prepare invalid message final MessageProperties messageProperties = createMessageProperties(MessageType.THING_REMOVED); - final Message message = messageConverter.toMessage(new byte[0], messageProperties); + final Message message = createMessage(new byte[0], messageProperties); assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no thingId was set") @@ -601,13 +661,13 @@ public class AmqpMessageHandlerServiceTest { VIRTUAL_HOST)); } - private MessageProperties getThingCreatedMessageProperties(String thingId) { + private MessageProperties getThingCreatedMessageProperties(final String thingId) { final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); messageProperties.setHeader(MessageHeaderKey.THING_ID, thingId); return messageProperties; } - private Message createMessage(Object object, MessageProperties messageProperties) { + private Message createMessage(final Object object, final MessageProperties messageProperties) { return messageConverter.toMessage(object, messageProperties); } } 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 bb3dc748f..d97a74f47 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 @@ -12,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -28,6 +29,7 @@ import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus; import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate; +import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMetadata; import org.eclipse.hawkbit.integration.listener.DeadletterListener; @@ -54,6 +56,9 @@ import org.springframework.amqp.rabbit.test.RabbitListenerTestHarness; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration; import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.CollectionUtils; + +import com.cronutils.utils.StringUtils; import io.qameta.allure.Step; @@ -92,7 +97,8 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt private T waitUntilIsPresent(final Callable> callable) { - createConditionFactory().until(() -> WithSpringAuthorityRule.runAsPrivileged(() -> callable.call().isPresent())); + createConditionFactory() + .until(() -> WithSpringAuthorityRule.runAsPrivileged(() -> callable.call().isPresent())); try { return WithSpringAuthorityRule.runAsPrivileged(() -> callable.call().get()); @@ -188,7 +194,8 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt protected void assertDmfDownloadAndUpdateRequest(final DmfDownloadAndUpdateRequest request, final Set softwareModules, final String controllerId) { - assertThat(softwareModules).is(new HamcrestCondition<>(SoftwareModuleJsonMatcher.containsExactly(request.getSoftwareModules()))); + assertThat(softwareModules) + .is(new HamcrestCondition<>(SoftwareModuleJsonMatcher.containsExactly(request.getSoftwareModules()))); request.getSoftwareModules().forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly( new DmfMetadata(TestdataFactory.VISIBLE_SM_MD_KEY, TestdataFactory.VISIBLE_SM_MD_VALUE))); final Target updatedTarget = waitUntilIsPresent(() -> targetManagement.getByControllerID(controllerId)); @@ -205,8 +212,13 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt assertAssignmentMessage(dsModules, controllerId, EventTopic.DOWNLOAD); } - protected void createAndSendThingCreated(final String target, final String tenant) { - final Message message = createTargetMessage(target, tenant); + protected void createAndSendThingCreated(final String controllerId, final String tenant) { + createAndSendThingCreated(controllerId, null, null, tenant); + } + + protected void createAndSendThingCreated(final String controllerId, final String name, + final Map attributes, final String tenant) { + final Message message = createTargetMessage(controllerId, name, attributes, tenant); getDmfClient().send(message); } @@ -254,32 +266,50 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt registerAndAssertTargetWithExistingTenant(controllerId, 1); } - protected void registerAndAssertTargetWithExistingTenant(final String target, + protected void registerAndAssertTargetWithExistingTenant(final String controllerId, final int existingTargetsAfterCreation) { - - registerAndAssertTargetWithExistingTenant(target, existingTargetsAfterCreation, TargetUpdateStatus.REGISTERED, - CREATED_BY); - + registerAndAssertTargetWithExistingTenant(controllerId, existingTargetsAfterCreation, + TargetUpdateStatus.REGISTERED, CREATED_BY); } - protected void registerAndAssertTargetWithExistingTenant(final String target, + protected void registerAndAssertTargetWithExistingTenant(final String controllerId, final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus, final String createdBy) { - createAndSendThingCreated(target, TENANT_EXIST); - final Target registeredTarget = waitUntilIsPresent(() -> targetManagement.getByControllerID(target)); + registerAndAssertTargetWithExistingTenant(controllerId, null, existingTargetsAfterCreation, + expectedTargetStatus, createdBy, null); + } + + protected void registerAndAssertTargetWithExistingTenant(final String controllerId, final String name, + final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus, + final String createdBy, final Map attributes) { + registerAndAssertTargetWithExistingTenant(controllerId, name, existingTargetsAfterCreation, + 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, TENANT_EXIST); + final Target registeredTarget = waitUntilIsPresent(fetchTarget::call); assertAllTargetsCount(existingTargetsAfterCreation); assertThat(registeredTarget).isNotNull(); - assertTarget(registeredTarget, expectedTargetStatus, createdBy); + 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, + expectedTargetStatus, null); + } + + protected void registerSameTargetAndAssertBasedOnVersion(final String controllerId, final String name, + final int existingTargetsAfterCreation, final TargetUpdateStatus expectedTargetStatus, + final Map attributes) { final int version = controllerManagement.getByControllerId(controllerId).get().getOptLockRevision(); - createAndSendThingCreated(controllerId, TENANT_EXIST); - final Target registeredTarget = waitUntilIsPresent(() -> findTargetBasedOnNewVersion(controllerId, version)); - assertAllTargetsCount(existingTargetsAfterCreation); - assertThat(registeredTarget).isNotNull(); - assertThat(registeredTarget.getUpdateStatus()).isEqualTo(expectedTargetStatus); + registerAndAssertTargetWithExistingTenant(controllerId, name, existingTargetsAfterCreation, + expectedTargetStatus, CREATED_BY, attributes, () -> findTargetBasedOnNewVersion(controllerId, version)); } private Optional findTargetBasedOnNewVersion(final String controllerId, final int version) { @@ -290,23 +320,43 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt return Optional.empty(); } - private void assertTarget(final Target target, final TargetUpdateStatus updateStatus, final String createdBy) { + 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(target.getAddress()) + .isEqualTo(IpUtil.createAmqpUri(getVirtualHost(), DmfTestConfiguration.REPLY_TO_EXCHANGE)); + assertThat(targetManagement.getControllerAttributes(target.getControllerId())).isEqualTo(attributes); } - protected Message createTargetMessage(final String target, final String tenant) { + protected Message createTargetMessage(final String controllerId, final String tenant) { + return createTargetMessage(controllerId, null, null, tenant); + } + + protected Message createTargetMessage(final String controllerId, final String name, + final Map attributes, final String tenant) { final MessageProperties messageProperties = createMessagePropertiesWithTenant(tenant); - messageProperties.getHeaders().put(MessageHeaderKey.THING_ID, target); + messageProperties.getHeaders().put(MessageHeaderKey.THING_ID, controllerId); messageProperties.getHeaders().put(MessageHeaderKey.TYPE, MessageType.THING_CREATED.toString()); messageProperties.setReplyTo(DmfTestConfiguration.REPLY_TO_EXCHANGE); - return createMessage(null, messageProperties); + DmfCreateThing payload = null; + if (!StringUtils.isEmpty(name) || !CollectionUtils.isEmpty(attributes)) { + payload = new DmfCreateThing(); + payload.setName(name); + + if (!CollectionUtils.isEmpty(attributes)) { + final DmfAttributeUpdate attributeUpdate = new DmfAttributeUpdate(); + attributeUpdate.getAttributes().putAll(attributes); + payload.setAttributeUpdate(attributeUpdate); + } + } + + return createMessage(payload, messageProperties); } protected Message createPingMessage(final String correlationId, final String tenant) { 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 bfdb18f8f..61f97ed8e 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 @@ -92,7 +92,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic assertPingReplyMessage(CORRELATION_ID); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); } @Test @@ -107,7 +107,63 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic registerAndAssertTargetWithExistingTenant(target2, 2); registerSameTargetAndAssertBasedOnVersion(target2, 2, TargetUpdateStatus.REGISTERED); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); + } + + @Test + @Description("Tests register target with name") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 2) }) + public void registerTargetWithName() { + final String controllerId = TARGET_PREFIX + "registerTargetWithName"; + final String name = "NonDefaultTargetName"; + registerAndAssertTargetWithExistingTenant(controllerId, name, 1, TargetUpdateStatus.REGISTERED, CREATED_BY, + null); + + registerSameTargetAndAssertBasedOnVersion(controllerId, name + "_updated", 1, TargetUpdateStatus.REGISTERED, + null); + + Mockito.verifyNoInteractions(getDeadletterListener()); + } + + @Test + @Description("Tests register target with attributes") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetPollEvent.class, count = 2) }) + public void registerTargetWithAttributes() { + final String controllerId = TARGET_PREFIX + "registerTargetWithAttributes"; + final Map attributes = new HashMap<>(); + attributes.put("testKey1", "testValue1"); + attributes.put("testKey2", "testValue2"); + + registerAndAssertTargetWithExistingTenant(controllerId, null, 1, TargetUpdateStatus.REGISTERED, CREATED_BY, + attributes); + + attributes.put("testKey3", "testValue3"); + registerSameTargetAndAssertBasedOnVersion(controllerId, null, 1, TargetUpdateStatus.REGISTERED, attributes); + + Mockito.verifyNoInteractions(getDeadletterListener()); + } + + @Test + @Description("Tests register target with name and attributes") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = TargetPollEvent.class, count = 2) }) + public void registerTargetWithNameAndAttributes() { + final String controllerId = TARGET_PREFIX + "registerTargetWithAttributes"; + final String name = "NonDefaultTargetName"; + final Map attributes = new HashMap<>(); + attributes.put("testKey1", "testValue1"); + attributes.put("testKey2", "testValue2"); + + registerAndAssertTargetWithExistingTenant(controllerId, name, 1, TargetUpdateStatus.REGISTERED, CREATED_BY, + attributes); + + attributes.put("testKey3", "testValue3"); + registerSameTargetAndAssertBasedOnVersion(controllerId, name + "_updated", 1, TargetUpdateStatus.REGISTERED, + attributes); + + Mockito.verifyNoInteractions(getDeadletterListener()); } @Test @@ -510,7 +566,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic // verify assertDownloadAndInstallMessage(distributionSet.getModules(), controllerId); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); } @Test @@ -537,7 +593,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic // verify assertDownloadMessage(distributionSet.getModules(), controllerId); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); } @Test @@ -564,7 +620,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic // verify assertDownloadAndInstallMessage(distributionSet.getModules(), controllerId); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); } @Test @@ -592,7 +648,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic // verify assertCancelActionMessage(getFirstAssignedActionId(distributionSetAssignmentResult), controllerId); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); } @Test @@ -836,15 +892,15 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic // verify final Message message = assertReplyMessageHeader(EventTopic.DOWNLOAD, controllerId); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); // get actionId from Message - Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); + final Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); // Send DOWNLOADED message createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DOWNLOADED); assertAction(actionId, 1, Status.RUNNING, Status.DOWNLOADED); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), null); } @@ -868,22 +924,22 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic // verify final Message message = assertReplyMessageHeader(EventTopic.DOWNLOAD, controllerId); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); // get actionId from Message - Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); + final Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); // Send DOWNLOADED message, should result in the action being closed createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DOWNLOADED); assertAction(actionId, 1, Status.RUNNING, Status.DOWNLOADED); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), null); // Send FINISHED message createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.FINISHED); assertAction(actionId, 2, Status.RUNNING, Status.DOWNLOADED, Status.FINISHED); - Mockito.verifyZeroInteractions(getDeadletterListener()); + Mockito.verifyNoInteractions(getDeadletterListener()); verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), distributionSet.getId()); } @@ -899,7 +955,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic final String controllerId = "dummy_target"; try { - for (Class exceptionClass : exceptionsThatShouldNotBeRequeued) { + for (final Class exceptionClass : exceptionsThatShouldNotBeRequeued) { doThrow(exceptionClass).when(mockedControllerManagement) .findOrRegisterTargetIfItDoesNotExist(eq(controllerId), any()); @@ -998,7 +1054,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic } private void assertEmptyReceiverQueueCount() { - assertThat(getAuthenticationMessageCount()).isEqualTo(0); + assertThat(getAuthenticationMessageCount()).isZero(); } private void verifyOneDeadLetterMessage() { @@ -1013,7 +1069,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic } private static String getJsonFieldFromBody(final byte[] body, final String fieldName) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); + final ObjectMapper objectMapper = new ObjectMapper(); final ObjectNode node = objectMapper.readValue(new String(body, Charset.defaultCharset()), ObjectNode.class); assertThat(node.has(fieldName)).isTrue(); return node.get(fieldName).asText(); diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java index 4e6d84c5e..b99ca293d 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java @@ -23,6 +23,9 @@ public class DmfCreateThing { @JsonProperty private String name; + @JsonProperty + private DmfAttributeUpdate attributeUpdate; + public String getName() { return name; } @@ -31,4 +34,11 @@ public class DmfCreateThing { this.name = name; } + public DmfAttributeUpdate getAttributeUpdate() { + return attributeUpdate; + } + + public void setAttributeUpdate(final DmfAttributeUpdate attributeUpdate) { + this.attributeUpdate = attributeUpdate; + } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java index 8591b2572..952c432e7 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java @@ -98,5 +98,4 @@ public interface Target extends NamedEntity { * {@link #getControllerAttributes()}. */ boolean isRequestControllerAttributes(); - }