diff --git a/docs/content/apis/dmf_api.md b/docs/content/apis/dmf_api.md index 1c024fe3f..48c64f580 100644 --- a/docs/content/apis/dmf_api.md +++ b/docs/content/apis/dmf_api.md @@ -58,6 +58,28 @@ Header ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- type=THING\_CREATED
tenant=tenant123
thingId=abc
sender=Lwm2m | content\_type=application/json
reply_to (optional) =sp.connector.replyTo + +### THING_REMOVED + +Message to request the deletion of a provisioning target. + +Header | Description | Type | Mandatory +-------------- | ------------------------------------------------ | ---------------------------- | ------------------------------------------------------------- +type | Type of the message | Fixed string "THING_REMOVED" | true +thingId | The ID of the registered provisioning target | String | true +tenant | The tenant this provisioning target belongs to | String | false + +Message Properties | Description | Type | Mandatory +------------------ | ---------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------- +content_type | The content type of the payload | String | true + +Example headers + +Header | MessageProperties +----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- +type=THING\_REMOVED
tenant=tenant123
thingId=abc | content\_type=application/json + + ### UPDATE_ATTRIBUTES Message to update target attributes. This message can be send in response to a _REQUEST_ATTRIBUTES_UPDATE_ event, sent by hawkBit. 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 9c36c5d7c..7bdbd8cdd 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 @@ -82,6 +82,8 @@ public class AmqpMessageHandlerService extends BaseAmqpService { private final SystemSecurityContext systemSecurityContext; + private static final String THING_ID_NULL = "ThingId is null"; + /** * Constructor. * @@ -156,6 +158,10 @@ public class AmqpMessageHandlerService extends BaseAmqpService { setTenantSecurityContext(tenant); registerTarget(message, virtualHost); break; + case THING_REMOVED: + setTenantSecurityContext(tenant); + deleteTarget(message); + break; case EVENT: checkContentTypeJson(message); setTenantSecurityContext(tenant); @@ -200,7 +206,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { * the ip of the target/thing */ private void registerTarget(final Message message, final String virtualHost) { - final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, "ThingId is null"); + final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL); final String replyTo = message.getMessageProperties().getReplyTo(); if (StringUtils.isEmpty(replyTo)) { @@ -274,8 +280,6 @@ public class AmqpMessageHandlerService extends BaseAmqpService { * * @param message * the incoming event message. - * @param topic - * the topic of the event. */ private void handleIncomingEvent(final Message message) { switch (EventTopic.valueOf(getStringHeaderKey(message, MessageHeaderKey.TOPIC, "EventTopic is null"))) { @@ -292,9 +296,14 @@ public class AmqpMessageHandlerService extends BaseAmqpService { } + private void deleteTarget(final Message message) { + final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL); + controllerManagement.deleteExistingTarget(thingId); + } + private void updateAttributes(final Message message) { final DmfAttributeUpdate attributeUpdate = convertMessage(message, DmfAttributeUpdate.class); - final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, "ThingId is null"); + final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL); controllerManagement.updateControllerAttributes(thingId, attributeUpdate.getAttributes(), getUpdateMode(attributeUpdate)); @@ -303,7 +312,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { /** * Method to update the action status of an action through the event. * - * @param actionUpdateStatus + * @param message * the object form the ampq message */ private void updateActionStatus(final Message message) { 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 8fe0132ce..723e4028c 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 @@ -9,8 +9,8 @@ package org.eclipse.hawkbit.amqp; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -82,10 +82,14 @@ import io.qameta.allure.Story; @Story("AmqpMessage Handler Service Test") public class AmqpMessageHandlerServiceTest { + private static final String FAIL_MESSAGE_AMQP_REJECT_REASON = AmqpRejectAndDontRequeueException.class.getSimpleName() + + " was expected, "; + private static final String SHA1 = "12345"; + private static final String VIRTUAL_HOST = "vHost"; private static final String TENANT = "DEFAULT"; private static final Long TENANT_ID = 123L; - private static final String CONTROLLLER_ID = "123"; + private static final String CONTROLLER_ID = "123"; private static final Long TARGET_ID = 123L; private AmqpMessageHandlerService amqpMessageHandlerService; @@ -162,11 +166,10 @@ public class AmqpMessageHandlerServiceTest { final MessageProperties messageProperties = new MessageProperties(); messageProperties.setContentType("xml"); final Message message = new Message(new byte[0], messageProperties); - try { - amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted due to worng content type"); - } catch (final AmqpRejectAndDontRequeueException e) { - } + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "due to wrong content type") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, + VIRTUAL_HOST)); } @Test @@ -185,12 +188,11 @@ public class AmqpMessageHandlerServiceTest { uriCaptor.capture())).thenReturn(targetMock); when(controllerManagementMock.findOldestActiveActionByTarget(any())).thenReturn(Optional.empty()); - amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, "vHost"); + 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://vHost/MyTest"); - + assertThat(uriCaptor.getValue().toString()).as("Uri is not right").isEqualTo("amqp://"+VIRTUAL_HOST+"/MyTest"); } @Test @@ -210,13 +212,12 @@ public class AmqpMessageHandlerServiceTest { when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), attributesCaptor.capture(), modeCaptor.capture())).thenReturn(null); - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + 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()); - } @Test @@ -235,7 +236,7 @@ public class AmqpMessageHandlerServiceTest { // send a message which does not specify a update mode Message message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + 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(); @@ -243,40 +244,36 @@ public class AmqpMessageHandlerServiceTest { // send a message which specifies update mode MERGE attributeUpdate.setMode(DmfUpdateMode.MERGE); message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + 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); // send a message which specifies update mode REPLACE attributeUpdate.setMode(DmfUpdateMode.REPLACE); message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + 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); // send a message which specifies update mode REMOVE attributeUpdate.setMode(DmfUpdateMode.REMOVE); message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + 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); - } @Test @Description("Tests the creation of a thing without a 'reply to' header in message.") - public void createThingWitoutReplyTo() { + public void createThingWithoutReplyTo() { final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED, null); messageProperties.setHeader(MessageHeaderKey.THING_ID, "1"); final Message message = messageConverter.toMessage("", messageProperties); - try { - amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted since no replyTo header was set"); - } catch (final AmqpRejectAndDontRequeueException exception) { - // test ok - exception was excepted - } - + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no replyTo header was set") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, + VIRTUAL_HOST)); } @Test @@ -284,12 +281,11 @@ public class AmqpMessageHandlerServiceTest { public void createThingWithoutID() { final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); final Message message = messageConverter.toMessage(new byte[0], messageProperties); - try { - amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted since no thingID was set"); - } catch (final AmqpRejectAndDontRequeueException exception) { - // test ok - exception was excepted - } + + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no thingId was set") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT, + VIRTUAL_HOST)); } @Test @@ -300,12 +296,9 @@ public class AmqpMessageHandlerServiceTest { messageProperties.setHeader(MessageHeaderKey.THING_ID, ""); final Message message = messageConverter.toMessage(new byte[0], messageProperties); - try { - amqpMessageHandlerService.onMessage(message, type, TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted due to unknown message type"); - } catch (final AmqpRejectAndDontRequeueException exception) { - // test ok - exception was excepted - } + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "due to unknown message type") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, type, TENANT, VIRTUAL_HOST)); } @Test @@ -313,27 +306,23 @@ public class AmqpMessageHandlerServiceTest { public void invalidEventTopic() { final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); final Message message = new Message(new byte[0], messageProperties); - try { - amqpMessageHandlerService.onMessage(message, "unknownMessageType", TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted due to unknown message type"); - } catch (final AmqpRejectAndDontRequeueException e) { - } - try { - messageProperties.setHeader(MessageHeaderKey.TOPIC, "wrongTopic"); - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted due to unknown topic"); - } catch (final AmqpRejectAndDontRequeueException e) { - } + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "due to unknown message type") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, "unknownMessageType", TENANT, + VIRTUAL_HOST)); + + messageProperties.setHeader(MessageHeaderKey.TOPIC, "wrongTopic"); + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "due to unknown topic") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, + VIRTUAL_HOST)); messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.CANCEL_DOWNLOAD.name()); - try { - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted because there was no event topic"); - } catch (final AmqpRejectAndDontRequeueException exception) { - // test ok - exception was excepted - } - + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "because there was no event topic") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, + VIRTUAL_HOST)); } @Test @@ -346,12 +335,10 @@ public class AmqpMessageHandlerServiceTest { final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, messageProperties); - try { - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted since no action id was set"); - } catch (final AmqpRejectAndDontRequeueException exception) { - // test ok - exception was excepted - } + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no action id was set") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, + VIRTUAL_HOST)); } @Test @@ -365,20 +352,17 @@ public class AmqpMessageHandlerServiceTest { final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, messageProperties); - try { - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); - fail("AmqpRejectAndDontRequeueException was excepeted since no action id was set"); - } catch (final AmqpRejectAndDontRequeueException exception) { - // test ok - exception was excepted - } - + assertThatExceptionOfType(AmqpRejectAndDontRequeueException.class) + .as(FAIL_MESSAGE_AMQP_REJECT_REASON + "since no action id was set") + .isThrownBy(() -> amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, + VIRTUAL_HOST)); } @Test @Description("Tests that an download request is denied for an artifact which does not exists") public void authenticationRequestDeniedForArtifactWhichDoesNotExists() { final MessageProperties messageProperties = createMessageProperties(null); - final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLLER_ID, + final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1("12345")); final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, messageProperties); @@ -397,7 +381,7 @@ public class AmqpMessageHandlerServiceTest { @Description("Tests that an download request is denied for an artifact which is not assigned to the requested target") public void authenticationRequestDeniedForArtifactWhichIsNotAssignedToTarget() { final MessageProperties messageProperties = createMessageProperties(null); - final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLLER_ID, + final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1("12345")); final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, messageProperties); @@ -419,7 +403,7 @@ public class AmqpMessageHandlerServiceTest { @Description("Tests that an download request is allowed for an artifact which exists and assigned to the requested target") public void authenticationRequestAllowedForArtifactWhichExistsAndAssignedToTarget() throws MalformedURLException { final MessageProperties messageProperties = createMessageProperties(null); - final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLLER_ID, + final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1("12345")); final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, messageProperties); @@ -473,7 +457,7 @@ public class AmqpMessageHandlerServiceTest { messageProperties); // test - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST); final ArgumentCaptor actionPropertiesCaptor = ArgumentCaptor.forClass(ActionProperties.class); final ArgumentCaptor targetCaptor = ArgumentCaptor.forClass(Target.class); @@ -485,7 +469,6 @@ public class AmqpMessageHandlerServiceTest { assertThat(actionProperties.getTenant()).as("event has tenant").isEqualTo("DEFAULT"); assertThat(targetCaptor.getValue().getControllerId()).as("event has wrong controller id").isEqualTo("target1"); assertThat(actionProperties.getId()).as("event has wrong action id").isEqualTo(22L); - } private DmfActionUpdateStatus createActionUpdateStatus(final DmfActionStatus status) { @@ -515,7 +498,7 @@ public class AmqpMessageHandlerServiceTest { private Action createActionWithTarget(final Long targetId, final Status status) throws IllegalAccessException { // is needed for the creation of targets - initalizeSecurityTokenGenerator(); + initializeSecurityTokenGenerator(); // Mock final Action actionMock = mock(Action.class); @@ -531,7 +514,7 @@ public class AmqpMessageHandlerServiceTest { return actionMock; } - private void initalizeSecurityTokenGenerator() throws IllegalAccessException { + private void initializeSecurityTokenGenerator() throws IllegalAccessException { final SecurityTokenGeneratorHolder instance = SecurityTokenGeneratorHolder.getInstance(); final Field[] fields = instance.getClass().getDeclaredFields(); for (final Field field : fields) { @@ -541,4 +524,34 @@ 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 = messageConverter.toMessage(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 = messageConverter.toMessage(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)); + } + } diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageType.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageType.java index 64cb28407..dfcd1a362 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageType.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageType.java @@ -29,13 +29,18 @@ public enum MessageType { */ THING_DELETED, + /** + * The request to delete a target. + */ + THING_REMOVED, + /** * DMF receiver health check type. */ PING, /** - * DMF receiver health check reponse type. + * DMF receiver health check response type. */ PING_RESPONSE diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 712ce8637..3a6d43dc2 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -48,14 +48,14 @@ public interface ControllerManagement { /** * Adds an {@link ActionStatus} for a cancel {@link Action} including * potential state changes for the target and the {@link Action} itself. - * + * * @param create * to be added * @return the updated {@link Action} - * + * * @throws EntityAlreadyExistsException * if a given entity already exists - * + * * @throws QuotaExceededException * if more than the allowed number of status entries or messages * per entry are inserted @@ -64,14 +64,14 @@ public interface ControllerManagement { * @throws ConstraintViolationException * if fields are not filled as specified. Check * {@link ActionStatusCreate} for field constraints. - * + * */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) Action addCancelActionStatus(@NotNull @Valid ActionStatusCreate create); /** * Retrieves assigned {@link SoftwareModule} of a target. - * + * * @param moduleId * of the {@link SoftwareModule} * @return {@link SoftwareModule} identified by ID @@ -82,7 +82,7 @@ public interface ControllerManagement { /** * Retrieves {@link SoftwareModuleMetadata} where * {@link SoftwareModuleMetadata#isTargetVisible()}. - * + * * @param moduleId * of the {@link SoftwareModule} * @return list of {@link SoftwareModuleMetadata} with maximum size of @@ -95,12 +95,12 @@ public interface ControllerManagement { /** * Simple addition of a new {@link ActionStatus} entry to the {@link Action} * . No state changes. - * + * * @param create * to add to the action - * + * * @return created {@link ActionStatus} entity - * + * * @throws QuotaExceededException * if more than the allowed number of status entries or messages * per entry are inserted @@ -138,15 +138,15 @@ public interface ControllerManagement { /** * Retrieves oldest {@link Action} that is active and assigned to a * {@link Target}. - * + * * For performance reasons this method does not throw - * {@link EntityNotFoundException} in case target with given controlelrId + * {@link EntityNotFoundException} in case target with given controllerId * does not exist but will return an {@link Optional#empty()} instead. * * @param controllerId * identifies the target to retrieve the actions from * @return a list of actions assigned to given target which are active - * + * */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) Optional findOldestActiveActionByTarget(@NotEmpty String controllerId); @@ -154,7 +154,7 @@ public interface ControllerManagement { /** * Retrieves all active actions which are assigned to the target with the * given controller ID. - * + * * @param pageable * pagination parameter * @param controllerId @@ -184,7 +184,7 @@ public interface ControllerManagement { * @param actionId * to be filtered on * @return the corresponding {@link Page} of {@link ActionStatus} - * + * * @throws EntityNotFoundException * if action with given ID does not exist */ @@ -217,7 +217,7 @@ public interface ControllerManagement { * of the the {@link SoftwareModule} that should be assigned to * the target * @return last {@link Action} for given combination - * + * * @throws EntityNotFoundException * if target with given ID does not exist * @@ -244,7 +244,7 @@ public interface ControllerManagement { /** * Returns the count to be used for reducing polling interval while calling - * {@link ControllerManagement#getPollingTimeForAction()}. + * {@link ControllerManagement#getPollingTimeForAction(long)}. * * @return configured value of * {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}. @@ -277,7 +277,7 @@ public interface ControllerManagement { * a target is allowed to download a given artifact because it has currently * assigned or had ever been assigned to the target and so it's visible to a * specific target e.g. for downloading. - * + * * @param controllerId * the ID of the target to check * @param sha1Hash @@ -286,7 +286,7 @@ public interface ControllerManagement { * @return {@code true} if the given target has currently or had ever a * relation to the given artifact through the action history, * otherwise {@code false} - * + * * @throws EntityNotFoundException * if target with given ID does not exist */ @@ -299,7 +299,7 @@ public interface ControllerManagement { * a target is allowed to download a given artifact because it has currently * assigned or had ever been assigned to the target and so it's visible to a * specific target e.g. for downloading. - * + * * @param targetId * the ID of the target to check * @param sha1Hash @@ -308,7 +308,7 @@ public interface ControllerManagement { * @return {@code true} if the given target has currently or had ever a * relation to the given artifact through the action history, * otherwise {@code false} - * + * * @throws EntityNotFoundException * if target with given ID does not exist */ @@ -325,7 +325,7 @@ public interface ControllerManagement { * for the status * @return the update action in case the status has been changed to * {@link Status#RETRIEVED} - * + * * @throws EntityNotFoundException * if action with given ID does not exist */ @@ -432,7 +432,7 @@ public interface ControllerManagement { /** * Updates given {@link Action} with its external id. - * + * * @param actionId * to be updated * @param externalRef @@ -451,4 +451,13 @@ public interface ControllerManagement { */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) List getActiveActionsByExternalRef(@NotNull List externalRefs); + + /** + * Delete a single target. + * + * @param controllerId + * of the target to delete + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + void deleteExistingTarget(@NotEmpty String controllerId); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index 83eea384f..1b6fea651 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -193,7 +193,7 @@ public class JpaControllerManagement implements ControllerManagement { /** * Returns the count to be used for reducing polling interval while calling - * {@link ControllerManagement#getPollingTimeForAction()}. + * {@link ControllerManagement#getPollingTimeForAction(long)}. * * @return configured value of * {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}. @@ -243,7 +243,7 @@ public class JpaControllerManagement implements ControllerManagement { * @param minimumEventInterval * for loading {@link DistributionSet#getModules()}. This * puts a lower bound to the timer value - * @param timerUnit + * @param timeUnit * representing the unit of time to be used for timer. */ EventTimer(final String defaultEventInterval, final String minimumEventInterval, final TemporalUnit timeUnit) { @@ -375,6 +375,13 @@ public class JpaControllerManagement implements ControllerManagement { return actionRepository.findByExternalRefInAndActive(externalRefs, true); } + @Override + public void deleteExistingTarget(@NotEmpty String controllerId) { + final Target target = targetRepository.findByControllerId(controllerId) + .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); + targetRepository.deleteById(target.getId()); + } + @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = ConcurrencyFailureException.class, exclude = EntityAlreadyExistsException.class, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) @@ -997,8 +1004,8 @@ public class JpaControllerManagement implements ControllerManagement { * Cancels given {@link Action} for this {@link Target}. The method will * immediately add a {@link Status#CANCELED} status to the action. However, * it might be possible that the controller will continue to work on the - * cancelation. The controller needs to acknowledge or reject the - * cancelation using {@link DdiRootController#postCancelActionFeedback}. + * cancellation. The controller needs to acknowledge or reject the + * cancellation using {@link DdiRootController#postCancelActionFeedback}. * * @param actionId * to be canceled diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index 324cafd03..082432e5f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -14,7 +14,6 @@ import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpre import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_MAX; import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; import static org.eclipse.hawkbit.repository.test.util.TestdataFactory.DEFAULT_CONTROLLER_ID; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -40,6 +39,7 @@ import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; +import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; 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; @@ -51,6 +51,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; @@ -89,7 +90,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { private RepositoryProperties repositoryProperties; @Test - @Description("Verifies that management get access react as specfied on calls for non existing entities by means " + @Description("Verifies that management get access react as specified on calls for non existing entities by means " + "of Optional not present.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 1)}) @@ -110,7 +111,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Verifies that management queries react as specfied on calls for non existing entities " + @Description("Verifies that management queries react as specified on calls for non existing entities " + " by means of throwing EntityNotFoundException.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 1)}) @@ -147,7 +148,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller confirms successfull update with FINISHED status.") + @Description("Controller confirms successful update with FINISHED status.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), @@ -170,7 +171,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller confirmation failes with invalid messages.") + @Description("Controller confirmation fails with invalid messages.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1), @@ -182,23 +183,23 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { simulateIntermediateStatusOnUpdate(actionId); assertThatExceptionOfType(ConstraintViolationException.class) + .as("set invalid description text should not be created") .isThrownBy(() -> controllerManagement.addUpdateActionStatus(entityFactory.actionStatus() - .create(actionId).status(Action.Status.FINISHED).message(INVALID_TEXT_HTML))) - .as("set invalid description text should not be created"); + .create(actionId).status(Action.Status.FINISHED).message(INVALID_TEXT_HTML))); assertThatExceptionOfType(ConstraintViolationException.class) + .as("set invalid description text should not be created") .isThrownBy(() -> controllerManagement.addUpdateActionStatus( entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED) - .messages(Arrays.asList("this is valid.", INVALID_TEXT_HTML)))) - .as("set invalid description text should not be created"); + .messages(Arrays.asList("this is valid.", INVALID_TEXT_HTML)))); assertThat(actionStatusRepository.count()).isEqualTo(6); assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(6); } @Test - @Description("Controller confirms successfull update with FINISHED status on a action that is on canceling. " - + "Reason: The decission to ignore the cancellation is in fact up to the controller.") + @Description("Controller confirms successful update with FINISHED status on a action that is on canceling. " + + "Reason: The decision to ignore the cancellation is in fact up to the controller.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), @@ -207,7 +208,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = TargetAttributesRequestedEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void controllerConfirmsUpdateWithFinishedAndIgnorsCancellationWithThat() { + public void controllerConfirmsUpdateWithFinishedAndIgnoresCancellationWithThat() { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); @@ -221,7 +222,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Update server rejects cancelation feedback if action is not in CANCELING state.") + @Description("Update server rejects cancellation feedback if action is not in CANCELING state.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1), @@ -230,24 +231,20 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { public void cancellationFeedbackRejectedIfActionIsNotInCanceling() { final Long actionId = createTargetAndAssignDs(); - try { - controllerManagement.addCancelActionStatus( - entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); - fail("Expected " + CancelActionNotAllowedException.class.getName()); - } catch (final CancelActionNotAllowedException e) { - // expected - } + assertThatExceptionOfType(CancelActionNotAllowedException.class) + .as("Expected " + CancelActionNotAllowedException.class.getName()) + .isThrownBy(() -> controllerManagement.addCancelActionStatus( + entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED))); assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RUNNING, true); assertThat(actionStatusRepository.count()).isEqualTo(1); assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(1); - } @Test - @Description("Controller confirms action cancelation with FINISHED status.") + @Description("Controller confirms action cancellation with FINISHED status.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), @@ -255,7 +252,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = CancelTargetAssignmentEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void controllerConfirmsActionCancelationWithFinished() { + public void controllerConfirmsActionCancellationWithFinished() { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); @@ -274,7 +271,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller confirms action cancelation with FINISHED status.") + @Description("Controller confirms action cancellation with FINISHED status.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), @@ -282,7 +279,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = CancelTargetAssignmentEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void controllerConfirmsActionCancelationWithCanceled() { + public void controllerConfirmsActionCancellationWithCanceled() { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); @@ -301,7 +298,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller rejects action cancelation with CANCEL_REJECTED status. Action goes back to RUNNING status as it expects " + @Description("Controller rejects action cancellation with CANCEL_REJECTED status. Action goes back to RUNNING status as it expects " + "that the controller will continue the original update.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @@ -310,7 +307,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = CancelTargetAssignmentEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void controllerRejectsActionCancelationWithReject() { + public void controllerRejectsActionCancellationWithReject() { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); @@ -329,7 +326,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller rejects action cancelation with ERROR status. Action goes back to RUNNING status as it expects " + @Description("Controller rejects action cancellation with ERROR status. Action goes back to RUNNING status as it expects " + "that the controller will continue the original update.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @@ -338,7 +335,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = CancelTargetAssignmentEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void controllerRejectsActionCancelationWithError() { + public void controllerRejectsActionCancellationWithError() { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); @@ -466,7 +463,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Verifies that assignement verification works based on SHA1 hash. By design it is not important which artifact " + @Description("Verifies that assignment verification works based on SHA1 hash. By design it is not important which artifact " + "is actually used for the check as long as they have an identical binary, i.e. same SHA1 hash. ") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 2), @@ -511,28 +508,28 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); assertThat(target.getId()).as("Target should be the equals").isEqualTo(sameTarget.getId()); - assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L); + assertThat(targetRepository.count()).as("Only 1 target should be registered").isEqualTo(1L); } @Test @Description("Tries to register a target with an invalid controller id") public void findOrRegisterTargetIfItDoesNotExistThrowsExceptionForInvalidControllerIdParam() { assertThatExceptionOfType(ConstraintViolationException.class) - .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist(null, LOCALHOST)) - .as("register target with null as controllerId should fail"); + .as("register target with null as controllerId should fail") + .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist(null, LOCALHOST)); assertThatExceptionOfType(ConstraintViolationException.class) - .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("", LOCALHOST)) - .as("register target with empty controllerId should fail"); + .as("register target with empty controllerId should fail") + .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("", LOCALHOST)); assertThatExceptionOfType(ConstraintViolationException.class) - .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist(" ", LOCALHOST)) - .as("register target with empty controllerId should fail"); + .as("register target with empty controllerId should fail") + .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist(" ", LOCALHOST)); assertThatExceptionOfType(ConstraintViolationException.class) + .as("register target with too long controllerId should fail") .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist( - RandomStringUtils.randomAlphabetic(Target.CONTROLLER_ID_MAX_SIZE + 1), LOCALHOST)) - .as("register target with too long controllerId should fail"); + RandomStringUtils.randomAlphabetic(Target.CONTROLLER_ID_MAX_SIZE + 1), LOCALHOST)); } @Test @@ -544,11 +541,13 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { ((JpaControllerManagement) controllerManagement).setTargetRepository(mockTargetRepository); try { - controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); - fail("Expected an ConcurrencyFailureException to be thrown!"); - } catch (final ConcurrencyFailureException e) { + assertThatExceptionOfType(ConcurrencyFailureException.class) + .as("Expected an ConcurrencyFailureException to be thrown!") + .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST)); + verify(mockTargetRepository, times(TX_RT_MAX)).findOne(any()); - } finally { + } + finally { // revert ((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository); } @@ -593,12 +592,13 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { when(mockTargetRepository.save(any())).thenThrow(EntityAlreadyExistsException.class); try { - controllerManagement.findOrRegisterTargetIfItDoesNotExist("1234", LOCALHOST); - fail("Expected an EntityAlreadyExistsException to be thrown!"); - } catch (final EntityAlreadyExistsException e) { + assertThatExceptionOfType(EntityAlreadyExistsException.class) + .as("Expected an EntityAlreadyExistsException to be thrown!") + .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("1234", LOCALHOST)); verify(mockTargetRepository, times(1)).findOne(any()); verify(mockTargetRepository, times(1)).save(any()); - } finally { + } + finally { // revert ((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository); } @@ -615,11 +615,13 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { when(mockTargetRepository.findOne(any())).thenThrow(RuntimeException.class); try { - controllerManagement.findOrRegisterTargetIfItDoesNotExist("aControllerId", LOCALHOST); - fail("Expected a RuntimeException to be thrown!"); - } catch (final RuntimeException e) { + assertThatExceptionOfType(RuntimeException.class) + .as("Expected a RuntimeException to be thrown!") + .isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("aControllerId", + LOCALHOST)); verify(mockTargetRepository, times(1)).findOne(any()); - } finally { + } + finally { // revert ((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository); } @@ -639,7 +641,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { set.getModules().stream().map(SoftwareModule::getId).collect(Collectors.toList())); assertThat(result).hasSize(3); - result.entrySet().forEach(entry -> assertThat(entry.getValue()).hasSize(1)); + result.forEach((key, value) -> assertThat(value).hasSize(1)); } @Test @@ -684,7 +686,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { Action.Status.ERROR, false); // try with enabled late feedback - should not make a difference as it - // only allows intermediate feedbacks and not multiple close + // only allows intermediate feedback and not multiple close repositoryProperties.setRejectActionStatusForClosedAction(false); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); @@ -699,7 +701,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller trys to finish an update process after it has been finished by an FINISHED action status.") + @Description("Controller tries to finish an update process after it has been finished by an FINISHED action status.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), @@ -720,7 +722,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { Action.Status.FINISHED, false); // try with enabled late feedback - should not make a difference as it - // only allows intermediate feedbacks and not multiple close + // only allows intermediate feedback and not multiple close repositoryProperties.setRejectActionStatusForClosedAction(false); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); @@ -735,7 +737,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller trys to send an update feedback after it has been finished which is reject as the repository is " + @Description("Controller tries to send an update feedback after it has been finished which is reject as the repository is " + "configured to reject that.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @@ -744,7 +746,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = TargetAttributesRequestedEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void sendUpdatesForFinishUpdateProcessDropedIfDisabled() { + public void sendUpdatesForFinishUpdateProcessDroppedIfDisabled() { repositoryProperties.setRejectActionStatusForClosedAction(true); final Action action = prepareFinishedUpdate(); @@ -762,7 +764,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller trys to send an update feedback after it has been finished which is accepted as the repository is " + @Description("Controller tries to send an update feedback after it has been finished which is accepted as the repository is " + "configured to accept them.") @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @@ -1001,24 +1003,24 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { testdataFactory.createTarget(controllerId); assertThatExceptionOfType(InvalidTargetAttributeException.class) + .as("Attribute with key too long should not be created") .isThrownBy(() -> controllerManagement.updateControllerAttributes(controllerId, - Collections.singletonMap(keyTooLong, valueValid), null)) - .as("Attribute with key too long should not be created"); + Collections.singletonMap(keyTooLong, valueValid), null)); assertThatExceptionOfType(InvalidTargetAttributeException.class) + .as("Attribute with key too long and value too long should not be created") .isThrownBy(() -> controllerManagement.updateControllerAttributes(controllerId, - Collections.singletonMap(keyTooLong, valueTooLong), null)) - .as("Attribute with key too long and value too long should not be created"); + Collections.singletonMap(keyTooLong, valueTooLong), null)); assertThatExceptionOfType(InvalidTargetAttributeException.class) + .as("Attribute with value too long should not be created") .isThrownBy(() -> controllerManagement.updateControllerAttributes(controllerId, - Collections.singletonMap(keyValid, valueTooLong), null)) - .as("Attribute with value too long should not be created"); + Collections.singletonMap(keyValid, valueTooLong), null)); assertThatExceptionOfType(InvalidTargetAttributeException.class) + .as("Attribute with key NULL should not be created") .isThrownBy(() -> controllerManagement.updateControllerAttributes(controllerId, - Collections.singletonMap(keyNull, valueValid), null)) - .as("Attribute with key NULL should not be created"); + Collections.singletonMap(keyNull, valueValid), null)); } @Test @@ -1253,32 +1255,26 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = ActionUpdatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForDownloadOnlyAction() { + public void quotaExceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForDownloadOnlyAction() { final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); testdataFactory.createTarget(); final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); assertThat(actionId).isNotNull(); - try { - IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); - fail("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates"); - } catch (final QuotaExceededException e) { - } + assertThatExceptionOfType(QuotaExceededException.class) + .as("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates") + .isThrownBy(() -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)))); - try { - IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR))); - fail("No QuotaExceededException thrown for too many ERROR updateActionStatus updates"); - } catch (final QuotaExceededException e) { - } + assertThatExceptionOfType(QuotaExceededException.class) + .as("No QuotaExceededException thrown for too many ERROR updateActionStatus updates") + .isThrownBy(() -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR)))); - try { - IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED))); - fail("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates"); - } catch (final QuotaExceededException e) { - } + assertThatExceptionOfType(QuotaExceededException.class) + .as("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates") + .isThrownBy(() -> IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED)))); } @Test @@ -1288,35 +1284,29 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), @Expect(type = SoftwareModuleCreatedEvent.class, count = 3)}) - public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForForced() { + public void quotaExceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForForced() { final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); final Long actionId = createTargetAndAssignDs(); assertThat(actionId).isNotNull(); - try { - IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); - fail("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates"); - } catch (final QuotaExceededException e) { - } + assertThatExceptionOfType(QuotaExceededException.class) + .as("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates") + .isThrownBy( ()-> IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)))); - try { - IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR))); - fail("No QuotaExceededException thrown for too many ERROR updateActionStatus updates"); - } catch (final QuotaExceededException e) { - } + assertThatExceptionOfType(QuotaExceededException.class) + .as("No QuotaExceededException thrown for too many ERROR updateActionStatus updates") + .isThrownBy(()->IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR)))); - try { - IntStream.range(0, maxMessages).forEach(i -> controllerManagement - .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED))); - fail("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates"); - } catch (final QuotaExceededException e) { - } + assertThatExceptionOfType(QuotaExceededException.class) + .as("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates") + .isThrownBy(()->IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED)))); } @Test - @Description("Verify that the attaching externalRef to an action is propery stored") + @Description("Verify that the attaching externalRef to an action is properly stored") public void updatedExternalRefOnActionIsReallyUpdated() { final List allExternalRef = new ArrayList<>(); final List allActionId = new ArrayList<>(); @@ -1324,15 +1314,15 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final DistributionSet knownDistributionSet = testdataFactory.createDistributionSet(); for (int i = 0; i < numberOfActions; i++) { final String knownControllerId = "controllerId" + i; - final String knownExternalref = "externalRefId" + i; + final String knownExternalRef = "externalRefId" + i; testdataFactory.createTarget(knownControllerId); final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(knownDistributionSet.getId(), knownControllerId); final Long actionId = getFirstAssignedActionId(assignmentResult); - controllerManagement.updateActionExternalRef(actionId, knownExternalref); + controllerManagement.updateActionExternalRef(actionId, knownExternalRef); - allExternalRef.add(knownExternalref); + allExternalRef.add(knownExternalRef); allActionId.add(actionId); } @@ -1346,11 +1336,9 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Verify that a null externalRef cannot be assigned to an action") public void externalRefCannotBeNull() { - try { - controllerManagement.updateActionExternalRef(1L, null); - fail("No ConstraintViolationException thrown when a null externalRef was set on an action"); - } catch (final ConstraintViolationException e) { - } + assertThatExceptionOfType(ConstraintViolationException.class) + .as("No ConstraintViolationException thrown when a null externalRef was set on an action") + .isThrownBy(() -> controllerManagement.updateActionExternalRef(1L, null)); } @Test @@ -1470,4 +1458,47 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(actionRepository.activeActionExistsForControllerId(controllerId)).isEqualTo(false); } + + @Test + @Description("Delete a target on requested target deletion from client side") + @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TargetDeletedEvent.class, count = 1)}) + public void deleteTargetWithValidThingId() { + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); + assertThat(target).as("target should not be null").isNotNull(); + assertThat(targetRepository.count()).as("target exists and is ready for deletion").isEqualTo(1L); + + controllerManagement.deleteExistingTarget(target.getControllerId()); + + assertThat(targetRepository.count()).as("target should not exist anymore").isEqualTo(0L); + } + + @Test + @Description("Delete a target with a non existing thingId") + @ExpectEvents({@Expect(type = TargetDeletedEvent.class, count = 0)}) + public void deleteTargetWithInvalidThingId() { + assertThatExceptionOfType(EntityNotFoundException.class) + .as("No EntityNotFoundException thrown when deleting a non-existing target") + .isThrownBy(() -> controllerManagement.deleteExistingTarget("BB")); + assertThat(targetRepository.count()).as("target should not exist").isEqualTo(0L); + } + + @Test + @Description("Delete a target after it has been deleted already") + @ExpectEvents({@Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TargetDeletedEvent.class, count = 1)}) + public void deleteTargetAfterItWasDeleted() { + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); + assertThat(target).as("target should not be null").isNotNull(); + assertThat(targetRepository.count()).as("target exists and is ready for deletion").isEqualTo(1L); + + controllerManagement.deleteExistingTarget(target.getControllerId()); + assertThat(targetRepository.count()).as("target should not exist anymore").isEqualTo(0L); + + assertThatExceptionOfType(EntityNotFoundException.class) + .as("No EntityNotFoundException thrown when deleting a non-existing target") + .isThrownBy(() -> controllerManagement.deleteExistingTarget(target.getControllerId())); + } }