From e8e203b69a821066782ac43847ef8a619fc06cc2 Mon Sep 17 00:00:00 2001 From: Stefan Schake Date: Thu, 28 Feb 2019 18:32:48 +0100 Subject: [PATCH] CBOR support for the DDI API (#797) * Add test for default content-type of DDI We want to add support for other content types to the DDI interface. To make sure we don't break devices that implicitly assume they will receive JSON without setting an Accept header, add a test for the current behavior. v2: Complete the assertion Signed-off-by: Stefan Schake * Add CBOR support for DDI API Concise Binary Object Representation (CBOR) is a binary data format optimized for small code and message size. Since Spring Boot 2, there is an autoconfigured data converter so we need to do little more than add the reference to the Jackson backend and advertise support in the DDI endpoints. Add tests to ensure all endpoints support the format. Fixes #748 Signed-off-by: Stefan Schake --- hawkbit-rest/hawkbit-ddi-api/pom.xml | 4 ++ .../ddi/rest/api/DdiRestConstants.java | 13 +++++ .../rest/api/DdiRootControllerRestApi.java | 19 ++++--- .../AbstractDDiApiIntegrationTest.java | 55 +++++++++++++++++++ .../rest/resource/DdiCancelActionTest.java | 29 ++++++++++ .../ddi/rest/resource/DdiConfigDataTest.java | 16 ++++++ .../rest/resource/DdiDeploymentBaseTest.java | 35 ++++++++++++ .../rest/resource/DdiRootControllerTest.java | 22 ++++++++ 8 files changed, 185 insertions(+), 8 deletions(-) diff --git a/hawkbit-rest/hawkbit-ddi-api/pom.xml b/hawkbit-rest/hawkbit-ddi-api/pom.xml index fafba9afa..932048a9d 100644 --- a/hawkbit-rest/hawkbit-ddi-api/pom.xml +++ b/hawkbit-rest/hawkbit-ddi-api/pom.xml @@ -29,6 +29,10 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + javax.validation validation-api diff --git a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java index bacb70ee4..cf04baa45 100644 --- a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java +++ b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java @@ -50,6 +50,19 @@ public final class DdiRestConstants { */ public static final String NO_ACTION_HISTORY = "0"; + /** + * Media type for CBOR content. Unfortunately, there is no other constant we + * can reuse - even the Jackson data converter simply hardcodes this. + */ + public static final String MEDIA_TYPE_CBOR = "application/cbor"; + + /** + * Media type for CBOR content with strings encoded as UTF-8. Technically + * redundant since CBOR always uses UTF-8, but Spring will append it + * regardless. + */ + public static final String MEDIA_TYPE_CBOR_UTF8 = "application/cbor;charset=UTF-8"; + private DdiRestConstants() { // constant class, private constructor. } diff --git a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java index 7b759c773..619ed1849 100644 --- a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java +++ b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java @@ -50,7 +50,7 @@ public interface DdiRootControllerRestApi { * @return the response */ @GetMapping(value = "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts", produces = { - MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR }) ResponseEntity> getSoftwareModulesArtifacts(@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId, @PathVariable("softwareModuleId") final Long softwareModuleId); @@ -66,7 +66,8 @@ public interface DdiRootControllerRestApi { * the HTTP request injected by spring * @return the response */ - @GetMapping(value = "/{controllerId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + @GetMapping(value = "/{controllerId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, + DdiRestConstants.MEDIA_TYPE_CBOR }) ResponseEntity getControllerBase(@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId); @@ -154,7 +155,7 @@ public interface DdiRootControllerRestApi { * @return the response */ @GetMapping(value = "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}", produces = { - MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR }) ResponseEntity getControllerBasedeploymentAction(@PathVariable("tenant") final String tenant, @PathVariable("controllerId") @NotEmpty final String controllerId, @PathVariable("actionId") @NotEmpty final Long actionId, @@ -178,7 +179,8 @@ public interface DdiRootControllerRestApi { * @return the response */ @PostMapping(value = "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/" - + DdiRestConstants.FEEDBACK, consumes = MediaType.APPLICATION_JSON_VALUE) + + DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE, + DdiRestConstants.MEDIA_TYPE_CBOR }) ResponseEntity postBasedeploymentActionFeedback(@Valid final DdiActionFeedback feedback, @PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId, @PathVariable("actionId") @NotEmpty final Long actionId); @@ -197,8 +199,8 @@ public interface DdiRootControllerRestApi { * * @return status of the request */ - @PutMapping(value = "/{controllerId}/" - + DdiRestConstants.CONFIG_DATA_ACTION, consumes = MediaType.APPLICATION_JSON_VALUE) + @PutMapping(value = "/{controllerId}/" + DdiRestConstants.CONFIG_DATA_ACTION, consumes = { + MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR }) ResponseEntity putConfigData(@Valid final DdiConfigData configData, @PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId); @@ -217,7 +219,7 @@ public interface DdiRootControllerRestApi { * @return the {@link DdiCancel} response */ @GetMapping(value = "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION + "/{actionId}", produces = { - MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR }) ResponseEntity getControllerCancelAction(@PathVariable("tenant") final String tenant, @PathVariable("controllerId") @NotEmpty final String controllerId, @PathVariable("actionId") @NotEmpty final Long actionId); @@ -241,7 +243,8 @@ public interface DdiRootControllerRestApi { */ @PostMapping(value = "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION + "/{actionId}/" - + DdiRestConstants.FEEDBACK, consumes = MediaType.APPLICATION_JSON_VALUE) + + DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE, + DdiRestConstants.MEDIA_TYPE_CBOR }) ResponseEntity postCancelActionFeedback(@Valid final DdiActionFeedback feedback, @PathVariable("tenant") final String tenant, @PathVariable("controllerId") @NotEmpty final String controllerId, diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java index b79f55a02..d54a0ed40 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java @@ -8,6 +8,11 @@ */ package org.eclipse.hawkbit.ddi.rest.resource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; + import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest; @@ -16,9 +21,59 @@ import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfigu import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import com.fasterxml.jackson.dataformat.cbor.CBORGenerator; +import com.fasterxml.jackson.dataformat.cbor.CBORParser; + @ContextConfiguration(classes = { DdiApiConfiguration.class, RestConfiguration.class, RepositoryApplicationConfiguration.class, TestConfiguration.class, TestSupportBinderAutoConfiguration.class }) @TestPropertySource(locations = "classpath:/ddi-test.properties") public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrationTest { + /** + * Convert JSON to a CBOR equivalent. + * + * @param json + * JSON object to convert + * @return Equivalent CBOR data + * @throws IOException + * Invalid JSON input + */ + protected static byte[] jsonToCbor(String json) throws IOException { + JsonFactory jsonFactory = new JsonFactory(); + JsonParser jsonParser = jsonFactory.createParser(json); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + CBORFactory cborFactory = new CBORFactory(); + CBORGenerator cborGenerator = cborFactory.createGenerator(out); + while (jsonParser.nextToken() != null) { + cborGenerator.copyCurrentEvent(jsonParser); + } + cborGenerator.flush(); + return out.toByteArray(); + } + + /** + * Convert CBOR to JSON equivalent. + * + * @param input + * CBOR data to convert + * @return Equivalent JSON string + * @throws IOException + * Invalid CBOR input + */ + protected static String cborToJson(byte[] input) throws IOException { + CBORFactory cborFactory = new CBORFactory(); + CBORParser cborParser = cborFactory.createParser(input); + JsonFactory jsonFactory = new JsonFactory(); + StringWriter stringWriter = new StringWriter(); + JsonGenerator jsonGenerator = jsonFactory.createGenerator(stringWriter); + while (cborParser.nextToken() != null) { + jsonGenerator.copyCurrentEvent(cborParser); + } + jsonGenerator.flush(); + return stringWriter.toString(); + } } diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java index bb2817f91..495abe560 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java @@ -23,6 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.ArrayList; import java.util.List; +import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -33,6 +34,8 @@ import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.junit.Test; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; +import org.springframework.integration.json.JsonPathUtils; +import org.springframework.test.util.JsonPathExpectationsHelper; import io.qameta.allure.Description; import io.qameta.allure.Feature; @@ -45,6 +48,32 @@ import io.qameta.allure.Story; @Story("Cancel Action Resource") public class DdiCancelActionTest extends AbstractDDiApiIntegrationTest { + @Test + @Description("Tests that the cancel action resource can be used with CBOR.") + public void cancelActionCbor() throws Exception { + final DistributionSet ds = testdataFactory.createDistributionSet(""); + final Target savedTarget = testdataFactory.createTarget(); + final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions() + .get(0); + final Action cancelAction = deploymentManagement.cancelAction(actionId); + + // check that we can get the cancel action as CBOR + byte[] result = mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + + cancelAction.getId(), tenantAware.getCurrentTenant()).accept(DdiRestConstants.MEDIA_TYPE_CBOR)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8)) + .andReturn().getResponse().getContentAsByteArray(); + assertThat(JsonPathUtils.evaluate(cborToJson(result), "$.id")).isEqualTo(String.valueOf(cancelAction.getId())); + assertThat(JsonPathUtils.evaluate(cborToJson(result), "$.cancelAction.stopId")).isEqualTo(String.valueOf(actionId)); + + // and submit feedback as CBOR + mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + + cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content( + jsonToCbor(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "proceeding"))) + .contentType(DdiRestConstants.MEDIA_TYPE_CBOR).accept(DdiRestConstants.MEDIA_TYPE_CBOR)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + @Test @Description("Test of the controller can continue a started update even after a cancel command if it so desires.") public void rootRsCancelActionButContinueAnyway() throws Exception { diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java index b0c60f6c3..8e675ce65 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; @@ -54,6 +55,21 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE + 1); private static final String VALUE_VALID = generateRandomStringWithLength(Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE); + @Test + @Description("Verify that config data can be uploaded as CBOR") + public void putConfigDataAsCbor() throws Exception { + testdataFactory.createTarget("4717"); + + final Map attributes = new HashMap<>(); + attributes.put(KEY_VALID, VALUE_VALID); + + mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant()) + .content(jsonToCbor(JsonBuilder.configData("", attributes, "closed").toString())) + .contentType(DdiRestConstants.MEDIA_TYPE_CBOR)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + assertThat(targetManagement.getControllerAttributes("4717")).isEqualTo(attributes); + } + @Test @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " + "are requested only once from the device.") diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java index 8e60a2cf8..5a342edc2 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.RandomUtils; import org.assertj.core.api.Condition; +import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; @@ -71,6 +72,40 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { private static final String HTTP_LOCALHOST = "http://localhost:8080/"; + @Test + @Description("Ensure that the deployment resource is available as CBOR") + public void deploymentResourceCbor() throws Exception { + final Target target = testdataFactory.createTarget(); + final DistributionSet distributionSet = testdataFactory.createDistributionSet(""); + + assignDistributionSet(distributionSet.getId(), target.getName()); + final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) + .getContent().get(0); + + // get deployment base + mvc.perform(get("/{tenant}/controller/v1/{target}/deploymentBase/" + uaction.getId(), + tenantAware.getCurrentTenant(), target.getControllerId()).accept(DdiRestConstants.MEDIA_TYPE_CBOR)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8)); + + final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId(); + testdataFactory.createArtifacts(softwareModuleId); + // get artifacts + mvc.perform(get("/{tenant}/controller/v1/{target}/softwaremodules/{softwareModuleId}/artifacts", + tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId) + .accept(DdiRestConstants.MEDIA_TYPE_CBOR)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8)); + + // submit feedback + final byte[] feedback = jsonToCbor( + JsonBuilder.deploymentActionFeedback(uaction.getId().toString(), "proceeding")); + mvc.perform(post("/{tenant}/controller/v1/{target}/deploymentBase/" + uaction.getId() + "/feedback", + tenantAware.getCurrentTenant(), target.getControllerId()).content(feedback) + .contentType(DdiRestConstants.MEDIA_TYPE_CBOR).accept(DdiRestConstants.MEDIA_TYPE_CBOR)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + @Test @Description("Ensures that artifacts are not found, when softare module does not exists.") public void artifactsNotFound() throws Exception { diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index e31fd0d6b..2a431802a 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -30,6 +30,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.Collections; import java.util.Map; +import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; @@ -59,6 +60,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import io.qameta.allure.Description; @@ -79,6 +81,26 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Autowired private HawkbitSecurityProperties securityProperties; + @Test + @Description("Ensure that the root poll resource is available as CBOR") + public void rootPollResourceCbor() throws Exception { + mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()) + .accept(DdiRestConstants.MEDIA_TYPE_CBOR)).andDo(MockMvcResultPrinter.print()) + .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8)).andExpect(status().isOk()); + } + + @Test + @Description("Ensures that the API returns JSON when no Accept header is specified by the client.") + public void apiReturnsJSONByDefault() throws Exception { + final MvcResult result = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON_UTF8)).andReturn(); + + // verify that we did not specify a content-type in the request, in case + // there are any default values + assertThat(result.getRequest().getHeader("Accept")).isNull(); + } + @Test @Description("Ensures that targets cannot be created e.g. in plug'n play scenarios when tenant does not exists but can be created if the tenant exists.") @WithUser(tenantId = "tenantDoesNotExists", allSpPermissions = true, authorities = { CONTROLLER_ROLE,