Moves DDI artifacts into a dedicated directory/artifact parent (#2002)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.ApiType;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.URLPlaceholder;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.URLPlaceholder.SoftwareData;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfig;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiMetadata;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiPolling;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||
import org.eclipse.hawkbit.repository.SystemManagement;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.rest.json.model.ResponseList;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Utility class for the DDI API.
|
||||
*/
|
||||
public final class DataConversionHelper {
|
||||
|
||||
// utility class, private constructor.
|
||||
private DataConversionHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static DdiConfirmationBase createConfirmationBase(final Target target, final Action activeAction,
|
||||
final DdiAutoConfirmationState autoConfirmationState, final TenantAware tenantAware) {
|
||||
final String controllerId = target.getControllerId();
|
||||
final DdiConfirmationBase confirmationBase = new DdiConfirmationBase(autoConfirmationState);
|
||||
if (autoConfirmationState.isActive()) {
|
||||
confirmationBase.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.deactivateAutoConfirmation(tenantAware.getCurrentTenant(), controllerId))
|
||||
.withRel(DdiRestConstants.AUTO_CONFIRM_DEACTIVATE).expand());
|
||||
} else {
|
||||
confirmationBase.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.activateAutoConfirmation(tenantAware.getCurrentTenant(), controllerId, null))
|
||||
.withRel(DdiRestConstants.AUTO_CONFIRM_ACTIVATE).expand());
|
||||
}
|
||||
if (activeAction != null && activeAction.isWaitingConfirmation()) {
|
||||
confirmationBase.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getConfirmationBaseAction(tenantAware.getCurrentTenant(), controllerId,
|
||||
activeAction.getId(), calculateEtag(activeAction), null))
|
||||
.withRel(DdiRestConstants.CONFIRMATION_BASE).expand());
|
||||
}
|
||||
|
||||
return confirmationBase;
|
||||
}
|
||||
|
||||
public static DdiControllerBase fromTarget(final Target target, final Action installedAction,
|
||||
final Action activeAction, final String defaultControllerPollTime, final TenantAware tenantAware) {
|
||||
final DdiControllerBase result = new DdiControllerBase(
|
||||
new DdiConfig(new DdiPolling(defaultControllerPollTime)));
|
||||
|
||||
if (activeAction != null) {
|
||||
if (activeAction.isWaitingConfirmation()) {
|
||||
result.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder
|
||||
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getConfirmationBaseAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
activeAction.getId(), calculateEtag(activeAction), null))
|
||||
.withRel(DdiRestConstants.CONFIRMATION_BASE).expand());
|
||||
|
||||
} else if (activeAction.isCancelingOrCanceled()) {
|
||||
result.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getControllerCancelAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
activeAction.getId()))
|
||||
.withRel(DdiRestConstants.CANCEL_ACTION).expand());
|
||||
} else {
|
||||
// we need to add the hashcode here of the actionWithStatus
|
||||
// because the action might
|
||||
// have changed from 'soft' to 'forced' type and we need to
|
||||
// change the payload of the
|
||||
// response because of eTags.
|
||||
result.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder
|
||||
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getControllerDeploymentBaseAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
activeAction.getId(), calculateEtag(activeAction), null))
|
||||
.withRel(DdiRestConstants.DEPLOYMENT_BASE_ACTION).expand());
|
||||
}
|
||||
}
|
||||
|
||||
if (installedAction != null && !installedAction.isActive()) {
|
||||
result.add(
|
||||
WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getControllerInstalledAction(tenantAware.getCurrentTenant(),
|
||||
target.getControllerId(), installedAction.getId(), null))
|
||||
.withRel(DdiRestConstants.INSTALLED_BASE_ACTION).expand());
|
||||
}
|
||||
|
||||
if (target.isRequestControllerAttributes()) {
|
||||
result.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.putConfigData(null, tenantAware.getCurrentTenant(), target.getControllerId()))
|
||||
.withRel(DdiRestConstants.CONFIG_DATA_ACTION).expand());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<DdiChunk> createChunks(final Target target, final Action uAction,
|
||||
final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement,
|
||||
final HttpRequest request, final ControllerManagement controllerManagement) {
|
||||
|
||||
final Map<Long, List<SoftwareModuleMetadata>> metadata = controllerManagement
|
||||
.findTargetVisibleMetaDataBySoftwareModuleId(uAction.getDistributionSet().getModules().stream()
|
||||
.map(SoftwareModule::getId).collect(Collectors.toList()));
|
||||
|
||||
return new ResponseList<>(uAction.getDistributionSet().getModules().stream()
|
||||
.map(module -> new DdiChunk(mapChunkLegacyKeys(module.getType().getKey()), module.getVersion(),
|
||||
module.getName(), module.isEncrypted() ? Boolean.TRUE : null,
|
||||
createArtifacts(target, module, artifactUrlHandler, systemManagement, request),
|
||||
mapMetadata(metadata.get(module.getId()))))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
}
|
||||
|
||||
static List<DdiArtifact> createArtifacts(final Target target, final SoftwareModule module,
|
||||
final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement,
|
||||
final HttpRequest request) {
|
||||
|
||||
return new ResponseList<>(module.getArtifacts().stream()
|
||||
.map(artifact -> createArtifact(target, artifactUrlHandler, artifact, systemManagement, request))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private static List<DdiMetadata> mapMetadata(final List<SoftwareModuleMetadata> metadata) {
|
||||
return CollectionUtils.isEmpty(metadata) ? null
|
||||
: metadata.stream().map(md -> new DdiMetadata(md.getKey(), md.getValue())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static String mapChunkLegacyKeys(final String key) {
|
||||
if ("application".equals(key)) {
|
||||
return "bApp";
|
||||
}
|
||||
if ("runtime".equals(key)) {
|
||||
return "jvm";
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private static DdiArtifact createArtifact(final Target target, final ArtifactUrlHandler artifactUrlHandler,
|
||||
final Artifact artifact, final SystemManagement systemManagement, final HttpRequest request) {
|
||||
final DdiArtifact file = new DdiArtifact();
|
||||
file.setHashes(new DdiArtifactHash(artifact.getSha1Hash(), artifact.getMd5Hash(), artifact.getSha256Hash()));
|
||||
file.setFilename(artifact.getFilename());
|
||||
file.setSize(artifact.getSize());
|
||||
|
||||
artifactUrlHandler
|
||||
.getUrls(new URLPlaceholder(systemManagement.getTenantMetadata().getTenant(),
|
||||
systemManagement.getTenantMetadata().getId(), target.getControllerId(), target.getId(),
|
||||
new SoftwareData(artifact.getSoftwareModule().getId(), artifact.getFilename(), artifact.getId(),
|
||||
artifact.getSha1Hash())),
|
||||
ApiType.DDI, request.getURI())
|
||||
.forEach(entry -> file.add(Link.of(entry.getRef()).withRel(entry.getRel()).expand()));
|
||||
|
||||
return file;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an etag for the given {@link Action} based on the entities
|
||||
* hashcode and the {@link Action#isHitAutoForceTime(long)} to reflect a
|
||||
* force switch.
|
||||
*
|
||||
* @param action to calculate the etag for
|
||||
* @return the etag
|
||||
*/
|
||||
private static int calculateEtag(final Action action) {
|
||||
final int prime = 31;
|
||||
int result = action.hashCode();
|
||||
int offsetPrime = action.isHitAutoForceTime(System.currentTimeMillis()) ? 1231 : 1237;
|
||||
offsetPrime = action.hasMaintenanceSchedule() && action.isMaintenanceWindowAvailable() ? 1249 : offsetPrime;
|
||||
|
||||
result = prime * result + offsetPrime;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import org.eclipse.hawkbit.rest.OpenApiConfiguration;
|
||||
import org.eclipse.hawkbit.rest.RestConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
/**
|
||||
* Enable {@link ComponentScan} in the resource package to setup all
|
||||
* {@link Controller} annotated classes and setup the REST-Resources for the
|
||||
* Direct Device Integration API.
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@Import({ RestConfiguration.class, OpenApiConfiguration.class })
|
||||
public class DdiApiConfiguration {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.eclipse.hawkbit.rest.OpenApiConfiguration;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(
|
||||
value = OpenApiConfiguration.HAWKBIT_SERVER_SWAGGER_ENABLED,
|
||||
havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public class DdiOpenApiConfiguration {
|
||||
|
||||
private static final String DDI_TOKEN_SEC_SCHEME_NAME = "DDI Target/GatewayToken Authentication";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "hawkbit.server.swagger.ddi.api.group.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public GroupedOpenApi ddiApi() {
|
||||
return GroupedOpenApi
|
||||
.builder()
|
||||
.group("Direct Device Integration API")
|
||||
.pathsToMatch("/{tenant}/controller/**")
|
||||
.addOpenApiCustomizer(openApi ->
|
||||
openApi
|
||||
.addSecurityItem(new SecurityRequirement().addList(DDI_TOKEN_SEC_SCHEME_NAME))
|
||||
.components(
|
||||
openApi
|
||||
.getComponents()
|
||||
.addSecuritySchemes(DDI_TOKEN_SEC_SCHEME_NAME,
|
||||
new SecurityScheme()
|
||||
.name("Authorization")
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.description("Format: (Target|Gateway)Token <token>"))))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,769 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancelActionToStop;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfigData;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBaseAction;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi;
|
||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||
import org.eclipse.hawkbit.repository.SystemManagement;
|
||||
import org.eclipse.hawkbit.repository.UpdateMode;
|
||||
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
||||
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
|
||||
import org.eclipse.hawkbit.repository.exception.SoftwareModuleNotAssignedToTargetException;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
|
||||
import org.eclipse.hawkbit.rest.util.HttpUtil;
|
||||
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.eclipse.hawkbit.util.IpUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.bus.BusProperties;
|
||||
import org.springframework.cloud.bus.ServiceMatcher;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* The {@link DdiRootController} of the hawkBit server DDI API that is queried
|
||||
* by the hawkBit controller in order to pull {@link Action}s that have to be
|
||||
* fulfilled and report status updates concerning the {@link Action} processing.
|
||||
*
|
||||
* Transactional (read-write) as all queries at least update the last poll time.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
|
||||
public class DdiRootController implements DdiRootControllerRestApi {
|
||||
|
||||
private static final String GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET = "given action ({}) is not assigned to given target ({}).";
|
||||
private static final String FALLBACK_REMARK = "Initiated using the Device Direct Integration API without providing a remark.";
|
||||
|
||||
@Autowired
|
||||
private ConfirmationManagement confirmationManagement;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ServiceMatcher serviceMatcher;
|
||||
|
||||
@Autowired
|
||||
private BusProperties bus;
|
||||
|
||||
@Autowired
|
||||
private ControllerManagement controllerManagement;
|
||||
|
||||
@Autowired
|
||||
private ArtifactManagement artifactManagement;
|
||||
|
||||
@Autowired
|
||||
private HawkbitSecurityProperties securityProperties;
|
||||
|
||||
@Autowired
|
||||
private TenantAware tenantAware;
|
||||
|
||||
@Autowired
|
||||
private SystemManagement systemManagement;
|
||||
|
||||
@Autowired
|
||||
private ArtifactUrlHandler artifactUrlHandler;
|
||||
|
||||
@Autowired
|
||||
private EntityFactory entityFactory;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<DdiArtifact>> getSoftwareModulesArtifacts(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId) {
|
||||
log.debug("getSoftwareModulesArtifacts({})", controllerId);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
|
||||
final SoftwareModule softwareModule = controllerManagement.getSoftwareModule(softwareModuleId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
|
||||
|
||||
return new ResponseEntity<>(
|
||||
DataConversionHelper.createArtifacts(target, softwareModule, artifactUrlHandler, systemManagement,
|
||||
new ServletServerHttpRequest(RequestResponseContextHolder.getHttpServletRequest())),
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiControllerBase> getControllerBase(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId) {
|
||||
log.debug("getControllerBase({})", controllerId);
|
||||
|
||||
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, IpUtil
|
||||
.getClientIpFromRequest(RequestResponseContextHolder.getHttpServletRequest(), securityProperties));
|
||||
final Action activeAction = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null);
|
||||
|
||||
final Action installedAction = controllerManagement.getInstalledActionByTarget(controllerId).orElse(null);
|
||||
|
||||
checkAndCancelExpiredAction(activeAction);
|
||||
|
||||
// activeAction
|
||||
return new ResponseEntity<>(DataConversionHelper.fromTarget(target, installedAction, activeAction,
|
||||
activeAction == null ? controllerManagement.getPollingTime()
|
||||
: controllerManagement.getPollingTimeForAction(activeAction.getId()),
|
||||
tenantAware), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<InputStream> downloadArtifact(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId,
|
||||
@PathVariable("fileName") final String fileName) {
|
||||
final ResponseEntity<InputStream> result;
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final SoftwareModule module = controllerManagement.getSoftwareModule(softwareModuleId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
|
||||
|
||||
if (checkModule(fileName, module)) {
|
||||
log.warn("Software module with id {} could not be found.", softwareModuleId);
|
||||
result = ResponseEntity.notFound().build();
|
||||
} else {
|
||||
// Artifact presence is ensured in 'checkModule'
|
||||
final Artifact artifact = module.getArtifactByFilename(fileName).orElseThrow(NoSuchElementException::new);
|
||||
final DbArtifact file = artifactManagement
|
||||
.loadArtifactBinary(artifact.getSha1Hash(), module.getId(), module.isEncrypted())
|
||||
.orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash()));
|
||||
|
||||
final String ifMatch = RequestResponseContextHolder.getHttpServletRequest().getHeader(HttpHeaders.IF_MATCH);
|
||||
if (ifMatch != null && !HttpUtil.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
|
||||
result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
|
||||
} else {
|
||||
final ActionStatus action = checkAndLogDownload(RequestResponseContextHolder.getHttpServletRequest(),
|
||||
target, module.getId());
|
||||
|
||||
final Long statusId = action.getId();
|
||||
|
||||
result = FileStreamingUtil.writeFileResponse(file, artifact.getFilename(), artifact.getCreatedAt(),
|
||||
RequestResponseContextHolder.getHttpServletResponse(),
|
||||
RequestResponseContextHolder.getHttpServletRequest(),
|
||||
(length, shippedSinceLastEvent,
|
||||
total) -> eventPublisher.publishEvent(new DownloadProgressEvent(
|
||||
tenantAware.getCurrentTenant(), statusId, shippedSinceLastEvent,
|
||||
serviceMatcher != null ? serviceMatcher.getBusId() : bus.getId())));
|
||||
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Exception squid:S3655 - Optional access is checked in checkModule
|
||||
// subroutine
|
||||
@SuppressWarnings("squid:S3655")
|
||||
public ResponseEntity<Void> downloadArtifactMd5(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId,
|
||||
@PathVariable("fileName") final String fileName) {
|
||||
final Target target = findTarget(controllerId);
|
||||
|
||||
final SoftwareModule module = controllerManagement.getSoftwareModule(softwareModuleId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
|
||||
|
||||
if (checkModule(fileName, module)) {
|
||||
log.warn("Software module with id {} could not be found.", softwareModuleId);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
final Artifact artifact = module.getArtifactByFilename(fileName)
|
||||
.orElseThrow(() -> new EntityNotFoundException(Artifact.class, fileName));
|
||||
|
||||
checkAndLogDownload(RequestResponseContextHolder.getHttpServletRequest(), target, module.getId());
|
||||
|
||||
try {
|
||||
FileStreamingUtil.writeMD5FileResponse(RequestResponseContextHolder.getHttpServletResponse(),
|
||||
artifact.getMd5Hash(), fileName);
|
||||
} catch (final IOException e) {
|
||||
log.error("Failed to stream MD5 File", e);
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiDeploymentBase> getControllerDeploymentBaseAction(
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") final Long actionId,
|
||||
@RequestParam(value = "c", required = false, defaultValue = "-1") final int resource,
|
||||
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) {
|
||||
log.debug("getControllerDeploymentBaseAction({},{})", controllerId, resource);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
checkAndCancelExpiredAction(action);
|
||||
|
||||
if (!action.isCancelingOrCanceled() && !action.isWaitingConfirmation()) {
|
||||
|
||||
final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount);
|
||||
|
||||
log.debug("Found an active UpdateAction for target {}. returning deployment: {}", controllerId, base);
|
||||
|
||||
controllerManagement.registerRetrieved(action.getId(), RepositoryConstants.SERVER_MESSAGE_PREFIX
|
||||
+ "Target retrieved update action and should start now the download.");
|
||||
|
||||
return new ResponseEntity<>(base, HttpStatus.OK);
|
||||
}
|
||||
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> postDeploymentBaseActionFeedback(@Valid @RequestBody final DdiActionFeedback feedback,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("postDeploymentBaseActionFeedback for target [{},{}]: {}", controllerId, actionId, feedback);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
if (action.isWaitingConfirmation()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!action.isActive()) {
|
||||
log.warn("Updating action {} with feedback {} not possible since action not active anymore.",
|
||||
action.getId(), feedback.getStatus());
|
||||
return new ResponseEntity<>(HttpStatus.GONE);
|
||||
}
|
||||
|
||||
controllerManagement.addUpdateActionStatus(generateUpdateStatus(feedback, controllerId, actionId));
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> putConfigData(@Valid @RequestBody final DdiConfigData configData,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId) {
|
||||
|
||||
controllerManagement.updateControllerAttributes(controllerId, configData.getData(), getUpdateMode(configData));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiCancel> getControllerCancelAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("getControllerCancelAction({})", controllerId);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
if (action.isCancelingOrCanceled()) {
|
||||
final DdiCancel cancel = new DdiCancel(String.valueOf(action.getId()),
|
||||
new DdiCancelActionToStop(String.valueOf(action.getId())));
|
||||
|
||||
log.debug("Found an active CancelAction for target {}. returning cancel: {}", controllerId, cancel);
|
||||
|
||||
controllerManagement.registerRetrieved(action.getId(), RepositoryConstants.SERVER_MESSAGE_PREFIX
|
||||
+ "Target retrieved cancel action and should start now the cancellation.");
|
||||
|
||||
return new ResponseEntity<>(cancel, HttpStatus.OK);
|
||||
}
|
||||
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> postCancelActionFeedback(@Valid @RequestBody final DdiActionFeedback feedback,
|
||||
@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("provideCancelActionFeedback for target [{}]: {}", controllerId, feedback);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
controllerManagement
|
||||
.addCancelActionStatus(generateActionCancelStatus(feedback, target, action.getId(), entityFactory));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiDeploymentBase> getControllerInstalledAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId, @PathVariable("actionId") final Long actionId,
|
||||
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) {
|
||||
log.debug("getControllerInstalledAction({})", controllerId);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
if (action.isActive() || action.isCancelingOrCanceled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount);
|
||||
|
||||
log.debug("Found an installed UpdateAction for target {}. returning deployment: {}", controllerId, base);
|
||||
return new ResponseEntity<>(base, HttpStatus.OK);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiConfirmationBase> getConfirmationBase(final String tenant, final String controllerId) {
|
||||
log.debug("getConfirmationBase is called [controllerId={}].", controllerId);
|
||||
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, IpUtil
|
||||
.getClientIpFromRequest(RequestResponseContextHolder.getHttpServletRequest(), securityProperties));
|
||||
final Action activeAction = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null);
|
||||
|
||||
final DdiAutoConfirmationState autoConfirmationState = getAutoConfirmationState(controllerId);
|
||||
|
||||
final DdiConfirmationBase confirmationBase = DataConversionHelper.createConfirmationBase(target, activeAction,
|
||||
autoConfirmationState, tenantAware);
|
||||
return new ResponseEntity<>(confirmationBase, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiConfirmationBaseAction> getConfirmationBaseAction(
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") final Long actionId,
|
||||
@RequestParam(value = "c", required = false, defaultValue = "-1") final int resource,
|
||||
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) {
|
||||
log.debug("getConfirmationBaseAction({},{})", controllerId, resource);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
checkAndCancelExpiredAction(action);
|
||||
|
||||
if (!action.isCancelingOrCanceled() && action.isWaitingConfirmation()) {
|
||||
|
||||
final DdiConfirmationBaseAction base = generateDdiConfirmationBase(target, action,
|
||||
actionHistoryMessageCount);
|
||||
|
||||
log.debug("Found an active UpdateAction for target {}. Returning confirmation: {}", controllerId, base);
|
||||
|
||||
return new ResponseEntity<>(base, HttpStatus.OK);
|
||||
}
|
||||
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> postConfirmationActionFeedback(
|
||||
@Valid @RequestBody final DdiConfirmationFeedback feedback, @PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("provideConfirmationActionFeedback with feedback [controllerId={}, actionId={}]: {}", controllerId,
|
||||
actionId, feedback);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
try {
|
||||
|
||||
switch (feedback.getConfirmation()) {
|
||||
case CONFIRMED:
|
||||
log.info("Controller confirmed the action (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getConfirmation());
|
||||
confirmationManagement.confirmAction(actionId, feedback.getCode(), feedback.getDetails());
|
||||
break;
|
||||
case DENIED:
|
||||
default:
|
||||
log.debug("Controller denied the action (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getConfirmation());
|
||||
confirmationManagement.denyAction(actionId, feedback.getCode(), feedback.getDetails());
|
||||
break;
|
||||
}
|
||||
} catch (final InvalidConfirmationFeedbackException e) {
|
||||
if (e.getReason() == InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED) {
|
||||
log.warn("Updating action {} with confirmation {} not possible since action not active anymore.",
|
||||
action.getId(), feedback.getConfirmation());
|
||||
return new ResponseEntity<>(HttpStatus.GONE);
|
||||
} else if (e.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION) {
|
||||
log.debug("Action is not waiting for confirmation, deny request.");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> activateAutoConfirmation(final String tenant, final String controllerId,
|
||||
final DdiActivateAutoConfirmation body) {
|
||||
final String initiator = body == null ? null : body.getInitiator();
|
||||
final String remark = body == null ? FALLBACK_REMARK : body.getRemark();
|
||||
log.debug("Activate auto-confirmation request for device '{}' with payload: [initiator='{}' | remark='{}'",
|
||||
controllerId, initiator, remark);
|
||||
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> deactivateAutoConfirmation(final String tenant, final String controllerId) {
|
||||
log.debug("Deactivate auto-confirmation request for device ‘{}‘", controllerId);
|
||||
confirmationManagement.deactivateAutoConfirmation(controllerId);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> setAsssignedOfflineVersion(@Valid @RequestBody DdiAssignedVersion ddiAssignedVersion,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId) {
|
||||
boolean updated = controllerManagement.updateOfflineAssignedVersion(controllerId,
|
||||
ddiAssignedVersion.getName(), ddiAssignedVersion.getVersion());
|
||||
if (updated) {
|
||||
return new ResponseEntity<>(HttpStatus.CREATED);
|
||||
}
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
private static boolean checkModule(final String fileName, final SoftwareModule module) {
|
||||
return module == null || module.getArtifactByFilename(fileName).isEmpty();
|
||||
}
|
||||
|
||||
private static HandlingType calculateDownloadType(final Action action) {
|
||||
if (action.isDownloadOnly() || action.isForcedOrTimeForced()) {
|
||||
return HandlingType.FORCED;
|
||||
}
|
||||
return HandlingType.ATTEMPT;
|
||||
}
|
||||
|
||||
private static DdiMaintenanceWindowStatus calculateMaintenanceWindow(final Action action) {
|
||||
if (action.hasMaintenanceSchedule()) {
|
||||
return action.isMaintenanceWindowAvailable() ? DdiMaintenanceWindowStatus.AVAILABLE
|
||||
: DdiMaintenanceWindowStatus.UNAVAILABLE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static HandlingType calculateUpdateType(final Action action, final HandlingType downloadType) {
|
||||
if (action.isDownloadOnly()) {
|
||||
return HandlingType.SKIP;
|
||||
} else if (action.hasMaintenanceSchedule()) {
|
||||
return action.isMaintenanceWindowAvailable() ? downloadType : HandlingType.SKIP;
|
||||
}
|
||||
return downloadType;
|
||||
}
|
||||
|
||||
private static void addMessageIfEmpty(final String text, final List<String> messages) {
|
||||
if (messages != null && messages.isEmpty()) {
|
||||
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + text + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static ActionStatusCreate generateActionCancelStatus(final DdiActionFeedback feedback, final Target target,
|
||||
final Long actionId, final EntityFactory entityFactory) {
|
||||
|
||||
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
|
||||
|
||||
final List<String> messages = new ArrayList<>();
|
||||
final Status status;
|
||||
switch (feedback.getStatus().getExecution()) {
|
||||
case CANCELED:
|
||||
status = handleCaseCancelCanceled(feedback, target, actionId, messages);
|
||||
break;
|
||||
case REJECTED:
|
||||
log.info("Target rejected the cancellation request (actionId: {}, controllerId: {}).", actionId,
|
||||
target.getControllerId());
|
||||
status = Status.CANCEL_REJECTED;
|
||||
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target rejected the cancellation request.");
|
||||
break;
|
||||
case CLOSED:
|
||||
status = handleCancelClosedCase(feedback, messages);
|
||||
break;
|
||||
default:
|
||||
status = Status.RUNNING;
|
||||
break;
|
||||
}
|
||||
|
||||
if (feedback.getStatus().getDetails() != null) {
|
||||
messages.addAll(feedback.getStatus().getDetails());
|
||||
}
|
||||
|
||||
final Integer code = feedback.getStatus().getCode();
|
||||
if (code != null) {
|
||||
actionStatusCreate.code(code);
|
||||
messages.add("Device reported status code: " + code);
|
||||
}
|
||||
|
||||
return actionStatusCreate.status(status).messages(messages);
|
||||
|
||||
}
|
||||
|
||||
private static Status handleCancelClosedCase(final DdiActionFeedback feedback, final List<String> messages) {
|
||||
final Status status;
|
||||
if (feedback.getStatus().getResult().getFinished() == FinalResult.FAILURE) {
|
||||
status = Status.ERROR;
|
||||
addMessageIfEmpty("Target was not able to complete cancellation", messages);
|
||||
} else {
|
||||
status = Status.CANCELED;
|
||||
addMessageIfEmpty("Cancellation confirmed", messages);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private static Status handleCaseCancelCanceled(final DdiActionFeedback feedback, final Target target,
|
||||
final Long actionId, final List<String> messages) {
|
||||
final Status status;
|
||||
log.error(
|
||||
"Target reported cancel for a cancel which is not supported by the server (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, target.getControllerId(), feedback.getStatus().getExecution());
|
||||
status = Status.WARNING;
|
||||
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX
|
||||
+ "Target reported cancel for a cancel which is not supported by the server.");
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the update mode from the given update message.
|
||||
*/
|
||||
private static UpdateMode getUpdateMode(final DdiConfigData configData) {
|
||||
final DdiUpdateMode mode = configData.getMode();
|
||||
if (mode != null) {
|
||||
return UpdateMode.valueOf(mode.name());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ActionStatus checkAndLogDownload(final HttpServletRequest request, final Target target, final Long module) {
|
||||
final Action action = controllerManagement
|
||||
.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), module)
|
||||
.orElseThrow(() -> new SoftwareModuleNotAssignedToTargetException(module, target.getControllerId()));
|
||||
final String range = request.getHeader("Range");
|
||||
|
||||
final String message;
|
||||
if (range != null) {
|
||||
message = RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads range " + range + " of: "
|
||||
+ request.getRequestURI();
|
||||
} else {
|
||||
message = RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads " + request.getRequestURI();
|
||||
}
|
||||
|
||||
return controllerManagement.addInformationalActionStatus(
|
||||
entityFactory.actionStatus().create(action.getId()).status(Status.DOWNLOAD).message(message));
|
||||
}
|
||||
|
||||
private ActionStatusCreate generateUpdateStatus(final DdiActionFeedback feedback, final String controllerId,
|
||||
final Long actionId) {
|
||||
|
||||
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
|
||||
|
||||
final List<String> messages = new ArrayList<>();
|
||||
|
||||
if (!CollectionUtils.isEmpty(feedback.getStatus().getDetails())) {
|
||||
messages.addAll(feedback.getStatus().getDetails());
|
||||
}
|
||||
|
||||
final Integer code = feedback.getStatus().getCode();
|
||||
if (code != null) {
|
||||
actionStatusCreate.code(code);
|
||||
messages.add("Device reported status code: " + code);
|
||||
}
|
||||
|
||||
final Status status;
|
||||
switch (feedback.getStatus().getExecution()) {
|
||||
case CANCELED:
|
||||
log.debug("Controller confirmed cancel (actionId: {}, controllerId: {}) as we got {} report.", actionId,
|
||||
controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.CANCELED;
|
||||
addMessageIfEmpty("Target confirmed cancellation.", messages);
|
||||
break;
|
||||
case REJECTED:
|
||||
log.info("Controller reported internal error (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.WARNING;
|
||||
addMessageIfEmpty("Target REJECTED update", messages);
|
||||
break;
|
||||
case CLOSED:
|
||||
status = handleClosedCase(feedback, controllerId, actionId, messages);
|
||||
break;
|
||||
case DOWNLOAD:
|
||||
log.debug("Controller confirmed status of download (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.DOWNLOAD;
|
||||
addMessageIfEmpty("Target confirmed download start", messages);
|
||||
break;
|
||||
case DOWNLOADED:
|
||||
log.debug("Controller confirmed download (actionId: {}, controllerId: {}) as we got {} report.", actionId,
|
||||
controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.DOWNLOADED;
|
||||
addMessageIfEmpty("Target confirmed download finished", messages);
|
||||
break;
|
||||
default:
|
||||
status = handleDefaultCase(feedback, controllerId, actionId, messages);
|
||||
break;
|
||||
}
|
||||
|
||||
return actionStatusCreate.status(status).messages(messages);
|
||||
}
|
||||
|
||||
private Status handleDefaultCase(final DdiActionFeedback feedback, final String controllerId, final Long actionId,
|
||||
final List<String> messages) {
|
||||
final Status status;
|
||||
log.debug("Controller reported intermediate status (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.RUNNING;
|
||||
addMessageIfEmpty("Target reported " + feedback.getStatus().getExecution(), messages);
|
||||
return status;
|
||||
}
|
||||
|
||||
private Status handleClosedCase(final DdiActionFeedback feedback, final String controllerId, final Long actionId,
|
||||
final List<String> messages) {
|
||||
final Status status;
|
||||
log.debug("Controller reported closed (actionId: {}, controllerId: {}) as we got {} report.", actionId,
|
||||
controllerId, feedback.getStatus().getExecution());
|
||||
if (feedback.getStatus().getResult().getFinished() == FinalResult.FAILURE) {
|
||||
status = Status.ERROR;
|
||||
addMessageIfEmpty("Target reported CLOSED with ERROR!", messages);
|
||||
} else {
|
||||
status = Status.FINISHED;
|
||||
addMessageIfEmpty("Target reported CLOSED with OK!", messages);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Target findTarget(final String controllerId) {
|
||||
return controllerManagement.getByControllerId(controllerId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId));
|
||||
}
|
||||
|
||||
private Action findActionForTarget(final Long actionId, final Target target) {
|
||||
final Action action = controllerManagement.findActionWithDetails(actionId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
|
||||
return verifyActionBelongsToTarget(action, target);
|
||||
}
|
||||
|
||||
private Action verifyActionBelongsToTarget(final Action action, final Target target) {
|
||||
if (!action.getTarget().getId().equals(target.getId())) {
|
||||
log.debug(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId());
|
||||
throw new EntityNotFoundException(
|
||||
"Not a valid action (" + action.getId() + ") for target: " + target.getControllerId(), null);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the action has a maintenance schedule defined but is no longer valid,
|
||||
* cancel the action.
|
||||
*
|
||||
* @param action is the {@link Action} to check.
|
||||
*/
|
||||
private void checkAndCancelExpiredAction(final Action action) {
|
||||
if (action != null && action.hasMaintenanceSchedule() && action.isMaintenanceScheduleLapsed()) {
|
||||
try {
|
||||
controllerManagement.cancelAction(action.getId());
|
||||
} catch (final CancelActionNotAllowedException e) {
|
||||
log.info("Cancel action not allowed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DdiDeploymentBase generateDdiDeploymentBase(final Target target, final Action action,
|
||||
final Integer actionHistoryMessageCount) {
|
||||
final DdiActionHistory actionHistory = generateDdiActionHistory(action, actionHistoryMessageCount).orElse(null);
|
||||
final DdiDeployment ddiDeployment = generateDdiDeployment(target, action);
|
||||
return new DdiDeploymentBase(Long.toString(action.getId()), ddiDeployment, actionHistory);
|
||||
}
|
||||
|
||||
private DdiConfirmationBaseAction generateDdiConfirmationBase(final Target target, final Action action,
|
||||
final Integer actionHistoryMessageCount) {
|
||||
final DdiActionHistory actionHistory = generateDdiActionHistory(action, actionHistoryMessageCount).orElse(null);
|
||||
final DdiDeployment ddiDeployment = generateDdiDeployment(target, action);
|
||||
return new DdiConfirmationBaseAction(Long.toString(action.getId()), ddiDeployment, actionHistory);
|
||||
}
|
||||
|
||||
private DdiDeployment generateDdiDeployment(final Target target, final Action action) {
|
||||
final List<DdiChunk> chunks = DataConversionHelper.createChunks(target, action, artifactUrlHandler,
|
||||
systemManagement, new ServletServerHttpRequest(RequestResponseContextHolder.getHttpServletRequest()),
|
||||
controllerManagement);
|
||||
final HandlingType downloadType = calculateDownloadType(action);
|
||||
final HandlingType updateType = calculateUpdateType(action, downloadType);
|
||||
final DdiMaintenanceWindowStatus maintenanceWindow = calculateMaintenanceWindow(action);
|
||||
return new DdiDeployment(downloadType, updateType, chunks, maintenanceWindow);
|
||||
}
|
||||
|
||||
private Optional<DdiActionHistory> generateDdiActionHistory(final Action action,
|
||||
final Integer actionHistoryMessageCount) {
|
||||
final List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
|
||||
actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY)
|
||||
: actionHistoryMessageCount);
|
||||
return actionHistoryMsgs.isEmpty() ? Optional.empty()
|
||||
: Optional.of(new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs));
|
||||
}
|
||||
|
||||
private DdiAutoConfirmationState getAutoConfirmationState(final String controllerId) {
|
||||
return confirmationManagement.getStatus(controllerId).map(status -> {
|
||||
final DdiAutoConfirmationState state = DdiAutoConfirmationState.active(status.getActivatedAt());
|
||||
state.setInitiator(status.getInitiator());
|
||||
state.setRemark(status.getRemark());
|
||||
log.trace("Returning state auto-conf state active [initiator='{}' | activatedAt={}] for device {}",
|
||||
controllerId, status.getInitiator(), status.getActivatedAt());
|
||||
return state;
|
||||
}).orElseGet(() -> {
|
||||
log.trace("Returning state auto-conf state disabled for device {}", controllerId);
|
||||
return DdiAutoConfirmationState.disabled();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORParser;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiProgress;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.test.TestConfiguration;
|
||||
import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest;
|
||||
import org.eclipse.hawkbit.rest.RestConfiguration;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
@ContextConfiguration(classes = { DdiApiConfiguration.class, RestConfiguration.class,
|
||||
RepositoryApplicationConfiguration.class, TestConfiguration.class })
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@TestPropertySource(locations = "classpath:/ddi-test.properties")
|
||||
public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrationTest {
|
||||
|
||||
public static final int HTTP_PORT = 8080;
|
||||
protected static final String HTTP_LOCALHOST = String.format("http://localhost:%s/", HTTP_PORT);
|
||||
protected static final String CONTROLLER_BASE = "/{tenant}/controller/v1/{controllerId}";
|
||||
|
||||
protected static final String SOFTWARE_MODULE_ARTIFACTS = CONTROLLER_BASE
|
||||
+ "/softwaremodules/{softwareModuleId}/artifacts";
|
||||
protected static final String DEPLOYMENT_BASE = CONTROLLER_BASE + "/deploymentBase/{actionId}";
|
||||
protected static final String DEPLOYMENT_FEEDBACK = DEPLOYMENT_BASE + "/feedback";
|
||||
protected static final String CANCEL_ACTION = CONTROLLER_BASE + "/cancelAction/{actionId}";
|
||||
protected static final String CANCEL_FEEDBACK = CANCEL_ACTION + "/feedback";
|
||||
protected static final String INSTALLED_BASE = CONTROLLER_BASE + "/installedBase/{actionId}";
|
||||
protected static final String INSTALLED_BASE_ROOT = CONTROLLER_BASE + "/installedBase";
|
||||
protected static final String CONFIRMATION_BASE = CONTROLLER_BASE + "/confirmationBase";
|
||||
protected static final String ACTIVATE_AUTO_CONFIRM = CONFIRMATION_BASE + "/activateAutoConfirm";
|
||||
protected static final String DEACTIVATE_AUTO_CONFIRM = CONFIRMATION_BASE + "/deactivateAutoConfirm";
|
||||
protected static final String CONFIRMATION_BASE_ACTION = CONTROLLER_BASE + "/confirmationBase/{actionId}";
|
||||
|
||||
protected static final String CONFIRMATION_FEEDBACK = CONFIRMATION_BASE_ACTION + "/feedback";
|
||||
|
||||
protected static final int ARTIFACT_SIZE = 5 * 1024;
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 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(final String json) throws IOException {
|
||||
final JsonFactory jsonFactory = new JsonFactory();
|
||||
final JsonParser jsonParser = jsonFactory.createParser(json);
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
final CBORFactory cborFactory = new CBORFactory();
|
||||
final 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(final byte[] input) throws IOException {
|
||||
final CBORFactory cborFactory = new CBORFactory();
|
||||
final CBORParser cborParser = cborFactory.createParser(input);
|
||||
final JsonFactory jsonFactory = new JsonFactory();
|
||||
final StringWriter stringWriter = new StringWriter();
|
||||
final JsonGenerator jsonGenerator = jsonFactory.createGenerator(stringWriter);
|
||||
while (cborParser.nextToken() != null) {
|
||||
jsonGenerator.copyCurrentEvent(cborParser);
|
||||
}
|
||||
jsonGenerator.flush();
|
||||
return stringWriter.toString();
|
||||
}
|
||||
|
||||
protected static ObjectMapper getMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
protected ResultActions postDeploymentFeedback(final String controllerId, final Long actionId, final String content,
|
||||
final ResultMatcher statusMatcher) throws Exception {
|
||||
return postDeploymentFeedback(MediaType.APPLICATION_JSON_UTF8, controllerId, actionId, content.getBytes(),
|
||||
statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions putInstalledBase(final String controllerId, final String content,
|
||||
final ResultMatcher statusMatcher) throws Exception {
|
||||
return mvc.perform(put(INSTALLED_BASE_ROOT, tenantAware.getCurrentTenant(), controllerId)
|
||||
.content(content.getBytes()).contentType(MediaType.APPLICATION_JSON_UTF8))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions postDeploymentFeedback(final MediaType mediaType, final String controllerId,
|
||||
final Long actionId, final byte[] content, final ResultMatcher statusMatcher) throws Exception {
|
||||
return mvc
|
||||
.perform(post(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||
.content(content).contentType(mediaType).accept(mediaType))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions postCancelFeedback(final String controllerId, final Long actionId, final String content,
|
||||
final ResultMatcher statusMatcher) throws Exception {
|
||||
return postCancelFeedback(MediaType.APPLICATION_JSON_UTF8, controllerId, actionId, content.getBytes(),
|
||||
statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions postCancelFeedback(final MediaType mediaType, final String controllerId,
|
||||
final Long actionId, final byte[] content, final ResultMatcher statusMatcher) throws Exception {
|
||||
return mvc
|
||||
.perform(post(CANCEL_FEEDBACK, tenantAware.getCurrentTenant(), controllerId, actionId).content(content)
|
||||
.contentType(mediaType).accept(mediaType))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions performGet(final String url, final MediaType mediaType, final ResultMatcher statusMatcher,
|
||||
final String... values) throws Exception {
|
||||
return mvc.perform(MockMvcRequestBuilders.get(url, values).accept(mediaType)
|
||||
.with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher)
|
||||
.andExpect(content().contentTypeCompatibleWith(mediaType));
|
||||
}
|
||||
|
||||
protected ResultActions getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||
final ResultActions resultActions = performGet(DEPLOYMENT_BASE, mediaType, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||
return verifyBasePayload("$.deployment", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||
downloadType, updateType);
|
||||
}
|
||||
|
||||
protected ResultActions getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final Action.ActionType actionType) throws Exception {
|
||||
return getAndVerifyDeploymentBasePayload(controllerId, mediaType, ds, artifact, artifactSignature, actionId,
|
||||
osModuleId, getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType));
|
||||
}
|
||||
|
||||
protected ResultActions getAndVerifyInstalledBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final Action.ActionType actionType) throws Exception {
|
||||
final ResultActions resultActions = performGet(INSTALLED_BASE, mediaType, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||
return verifyBasePayload("$.deployment", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||
getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType));
|
||||
}
|
||||
|
||||
protected String installedBaseLink(final String controllerId, final String actionId) {
|
||||
return HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/installedBase/" + actionId;
|
||||
}
|
||||
|
||||
protected String deploymentBaseLink(final String controllerId, final String actionId) {
|
||||
return HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/deploymentBase/" + actionId;
|
||||
}
|
||||
|
||||
protected String getJsonRejectedCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.REJECTED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("rejected"));
|
||||
}
|
||||
|
||||
protected String getJsonRejectedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.REJECTED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("rejected"));
|
||||
}
|
||||
|
||||
protected String getJsonDownloadDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.DOWNLOAD, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("download"));
|
||||
}
|
||||
|
||||
protected String getJsonDownloadedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.DOWNLOADED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("download"));
|
||||
}
|
||||
|
||||
protected String getJsonCanceledCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CANCELED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("canceled"));
|
||||
}
|
||||
|
||||
protected String getJsonCanceledDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CANCELED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("canceled"));
|
||||
}
|
||||
|
||||
protected String getJsonScheduledCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.SCHEDULED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("scheduled"));
|
||||
}
|
||||
|
||||
protected String getJsonScheduledDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.SCHEDULED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("scheduled"));
|
||||
}
|
||||
|
||||
protected String getJsonResumedCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.RESUMED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("resumed"));
|
||||
}
|
||||
|
||||
protected String getJsonResumedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.RESUMED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("resumed"));
|
||||
}
|
||||
|
||||
protected String getJsonProceedingCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("proceeding"));
|
||||
}
|
||||
|
||||
protected String getJsonProceedingDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("proceeding"));
|
||||
}
|
||||
|
||||
protected String getJsonClosedCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("closed"));
|
||||
}
|
||||
|
||||
protected String getJsonClosedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("closed"));
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
|
||||
final DdiResult.FinalResult finalResult) throws JsonProcessingException {
|
||||
return getJsonActionFeedback(executionStatus, finalResult,
|
||||
Collections.singletonList(RandomStringUtils.randomAlphanumeric(1000)));
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus, final DdiResult ddiResult,
|
||||
final List<String> messages) throws JsonProcessingException {
|
||||
final DdiStatus ddiStatus = new DdiStatus(executionStatus, ddiResult, null, messages);
|
||||
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
|
||||
final DdiResult.FinalResult finalResult, final List<String> messages) throws JsonProcessingException {
|
||||
return getJsonActionFeedback(executionStatus, finalResult, null, messages);
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
|
||||
final DdiResult.FinalResult finalResult, final Integer code, final List<String> messages) throws JsonProcessingException {
|
||||
final DdiStatus ddiStatus = new DdiStatus(executionStatus, new DdiResult(finalResult, new DdiProgress(2, 5)), code,
|
||||
messages);
|
||||
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
|
||||
}
|
||||
|
||||
protected String getJsonConfirmationFeedback(final DdiConfirmationFeedback.Confirmation confirmation,
|
||||
final Integer code, final List<String> messages) throws JsonProcessingException {
|
||||
return objectMapper.writeValueAsString(new DdiConfirmationFeedback(confirmation, code, messages));
|
||||
}
|
||||
|
||||
protected String getJsonInstalledBase(String name, String version) throws JsonProcessingException {
|
||||
return objectMapper.writeValueAsString(new DdiAssignedVersion(name, version));
|
||||
}
|
||||
|
||||
protected ResultActions getAndVerifyConfirmationBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||
final ResultActions resultActions = performGet(CONFIRMATION_BASE_ACTION, mediaType, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||
return verifyBasePayload("$.confirmation", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||
downloadType, updateType);
|
||||
}
|
||||
|
||||
static void implicitLock(final DistributionSet set) {
|
||||
((JpaDistributionSet) set).setOptLockRevision(set.getOptLockRevision() + 1);
|
||||
}
|
||||
|
||||
private static String getDownloadAndUploadType(final Action.ActionType actionType) {
|
||||
if (Action.ActionType.FORCED.equals(actionType)) {
|
||||
return "forced";
|
||||
}
|
||||
return "attempt";
|
||||
}
|
||||
|
||||
private ResultActions verifyBasePayload(final String prefix, final ResultActions resultActions, final String controllerId,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||
return resultActions.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId))))
|
||||
.andExpect(jsonPath(prefix + ".download", equalTo(downloadType)))
|
||||
.andExpect(jsonPath(prefix + ".update", equalTo(updateType)))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='jvm')].name",
|
||||
contains(ds.findFirstModuleByType(runtimeType).get().getName())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='jvm')].version",
|
||||
contains(ds.findFirstModuleByType(runtimeType).get().getVersion())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].name",
|
||||
contains(ds.findFirstModuleByType(osType).get().getName())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].version",
|
||||
contains(ds.findFirstModuleByType(osType).get().getVersion())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].size", contains(ARTIFACT_SIZE)))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].filename",
|
||||
contains(artifact.getFilename())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.md5",
|
||||
contains(artifact.getMd5Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha1",
|
||||
contains(artifact.getSha1Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha256",
|
||||
contains(artifact.getSha256Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.download-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()
|
||||
+ "/download")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.md5sum-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()
|
||||
+ "/download.MD5SUM")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].size", contains(ARTIFACT_SIZE)))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].filename",
|
||||
contains(artifactSignature.getFilename())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.md5",
|
||||
contains(artifactSignature.getMd5Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha1",
|
||||
contains(artifactSignature.getSha1Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha256",
|
||||
contains(artifactSignature.getSha256Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.download-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()
|
||||
+ "/download")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.md5sum-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()
|
||||
+ "/download.MD5SUM")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].version",
|
||||
contains(ds.findFirstModuleByType(appType).get().getVersion())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].metadata").doesNotExist())
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].name")
|
||||
.value(ds.findFirstModuleByType(appType).get().getName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.eclipse.hawkbit.ddi.rest.resource.DdiArtifactDownloadTest.DownloadTestConfiguration;
|
||||
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
|
||||
import org.eclipse.hawkbit.repository.test.util.WithUser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
/**
|
||||
* Test artifact downloads from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Artifact Download Resource")
|
||||
@SpringBootTest(classes = { DownloadTestConfiguration.class })
|
||||
public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static volatile int downLoadProgress = 0;
|
||||
private static volatile long shippedBytes = 0;
|
||||
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests non allowed requests on the artifact ressource, e.g. invalid URI, wrong if-match, wrong command.")
|
||||
public void invalidRequestsOnArtifactResource() throws Exception {
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final List<Target> targets = Arrays.asList(target);
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
// create artifact
|
||||
final int artifactSize = 5 * 1024;
|
||||
final byte random[] = RandomUtils.nextBytes(artifactSize);
|
||||
final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random),
|
||||
ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize));
|
||||
|
||||
assignDistributionSet(ds, targets);
|
||||
|
||||
// no artifact available
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455",
|
||||
target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound());
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455.MD5SUM",
|
||||
target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound());
|
||||
|
||||
// SM does not exist
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}",
|
||||
target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound());
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}.MD5SUM",
|
||||
target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound());
|
||||
|
||||
// test now consistent data to test allowed methods
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header(HttpHeaders.IF_MATCH, artifact.getSha1Hash()))
|
||||
.andExpect(status().isOk());
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// test failed If-match
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header(HttpHeaders.IF_MATCH, "fsjkhgjfdhg"))
|
||||
.andExpect(status().isPreconditionFailed());
|
||||
|
||||
// test invalid range
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header("Range", "bytes=1-10,hdsfjksdh"))
|
||||
.andExpect(header().string("Content-Range", "bytes */" + 5 * 1024))
|
||||
.andExpect(status().isRequestedRangeNotSatisfiable());
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header("Range", "bytes=100-10"))
|
||||
.andExpect(header().string("Content-Range", "bytes */" + 5 * 1024))
|
||||
.andExpect(status().isRequestedRangeNotSatisfiable());
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true)
|
||||
@Description("Tests valid downloads through the artifact resource by identifying the artifact not by ID but file name.")
|
||||
public void downloadArtifactThroughFileName() throws Exception {
|
||||
downLoadProgress = 1;
|
||||
shippedBytes = 0;
|
||||
assertThat(softwareModuleManagement.findAll(PAGE)).hasSize(0);
|
||||
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final List<Target> targets = Arrays.asList(target);
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
// create artifact
|
||||
final int artifactSize = (int) quotaManagement.getMaxArtifactSize();
|
||||
final byte random[] = RandomUtils.nextBytes(artifactSize);
|
||||
final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random),
|
||||
ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize));
|
||||
|
||||
// download fails as artifact is not yet assigned
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
target.getControllerId(), getOsModule(ds), artifact.getFilename())).andExpect(status().isNotFound());
|
||||
|
||||
// now assign and download successful
|
||||
assignDistributionSet(ds, targets);
|
||||
final MvcResult result = mvc.perform(get(
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=" + artifact.getFilename()))
|
||||
.andReturn();
|
||||
|
||||
assertTrue(
|
||||
Arrays.equals(result.getResponse().getContentAsByteArray(), random),
|
||||
"The same file that was uploaded is expected when downloaded");
|
||||
|
||||
// download complete
|
||||
assertThat(downLoadProgress).isEqualTo(10);
|
||||
assertThat(shippedBytes).isEqualTo(artifactSize);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests valid MD5SUm file downloads through the artifact resource by identifying the artifact by ID.")
|
||||
public void downloadMd5sumThroughControllerApi() throws Exception {
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
// create artifact
|
||||
final int artifactSize = 5 * 1024;
|
||||
final byte random[] = RandomUtils.nextBytes(artifactSize);
|
||||
final Artifact artifact = artifactManagement.create(
|
||||
new ArtifactUpload(new ByteArrayInputStream(random), getOsModule(ds), "file1", false, artifactSize));
|
||||
|
||||
assignDistributionSet(ds, target);
|
||||
|
||||
// download
|
||||
final MvcResult result = mvc.perform(get(
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isOk()).andExpect(header().string("Content-Disposition",
|
||||
"attachment;filename=" + artifact.getFilename() + ".MD5SUM"))
|
||||
.andReturn();
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray())
|
||||
.isEqualTo((artifact.getMd5Hash() + " " + artifact.getFilename()).getBytes(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUser(principal = TestdataFactory.DEFAULT_CONTROLLER_ID, authorities = "ROLE_CONTROLLER", allSpPermissions = true)
|
||||
@Description("Test various HTTP range requests for artifact download, e.g. chunk download or download resume.")
|
||||
public void rangeDownloadArtifact() throws Exception {
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final List<Target> targets = Arrays.asList(target);
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final int resultLength = (int) quotaManagement.getMaxArtifactSize();
|
||||
|
||||
// create artifact
|
||||
final byte random[] = RandomUtils.nextBytes(resultLength);
|
||||
final Artifact artifact = artifactManagement.create(
|
||||
new ArtifactUpload(new ByteArrayInputStream(random), getOsModule(ds), "file1", false, resultLength));
|
||||
|
||||
assertThat(random.length).isEqualTo(resultLength);
|
||||
|
||||
// now assign and download successful
|
||||
assignDistributionSet(ds, targets);
|
||||
|
||||
final int range = resultLength / 50;
|
||||
|
||||
// full file download with standard range request
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
for (int i = 0; i < resultLength / range; i++) {
|
||||
final String rangeString = "" + i * range + "-" + ((i + 1) * range - 1);
|
||||
|
||||
final MvcResult result = mvc.perform(get(
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1").header("Range",
|
||||
"bytes=" + rangeString))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().longValue("Content-Length", range))
|
||||
.andExpect(header().string("Content-Range", "bytes " + rangeString + "/" + resultLength))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
outputStream.write(result.getResponse().getContentAsByteArray());
|
||||
}
|
||||
|
||||
assertThat(outputStream.toByteArray()).isEqualTo(random);
|
||||
|
||||
// return last 1000 Bytes
|
||||
MvcResult result = mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=-1000"))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().longValue("Content-Length", 1000))
|
||||
.andExpect(header().string("Content-Range",
|
||||
"bytes " + (resultLength - 1000) + "-" + (resultLength - 1) + "/" + resultLength))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray())
|
||||
.isEqualTo(Arrays.copyOfRange(random, resultLength - 1000, resultLength));
|
||||
|
||||
// skip first 1000 Bytes and return the rest
|
||||
result = mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=1000-"))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().longValue("Content-Length", resultLength - 1000))
|
||||
.andExpect(header().string("Content-Range",
|
||||
"bytes " + 1000 + "-" + (resultLength - 1) + "/" + resultLength))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray())
|
||||
.isEqualTo(Arrays.copyOfRange(random, 1000, resultLength));
|
||||
|
||||
// Start download from file end fails
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=" + random.length + "-"))
|
||||
.andExpect(status().isRequestedRangeNotSatisfiable())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().string("Content-Range", "bytes */" + random.length))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"));
|
||||
|
||||
// multipart download - first 20 bytes in 2 parts
|
||||
result = mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=0-9,10-19"))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType("multipart/byteranges; boundary=THIS_STRING_SEPARATES_MULTIPART"))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
outputStream.reset();
|
||||
|
||||
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.write(("Content-Range: bytes 0-9/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.write(Arrays.copyOfRange(random, 0, 10));
|
||||
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream
|
||||
.write(("Content-Range: bytes 10-19/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.write(Arrays.copyOfRange(random, 10, 20));
|
||||
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART--".getBytes(StandardCharsets.ISO_8859_1));
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray()).isEqualTo(outputStream.toByteArray());
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class DownloadTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public Listener cancelEventHandlerStubBean() {
|
||||
return new Listener();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Listener {
|
||||
|
||||
@EventListener(classes = DownloadProgressEvent.class)
|
||||
public static void listen(final DownloadProgressEvent event) {
|
||||
downLoadProgress++;
|
||||
shippedBytes += event.getShippedBytesSinceLast();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.integration.json.JsonPathUtils;
|
||||
|
||||
/**
|
||||
* Test cancel action from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Cancel Action Resource")
|
||||
class DdiCancelActionTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
ActionStatusRepository actionStatusRepository;
|
||||
|
||||
@Test
|
||||
@Description("Tests that the cancel action resource can be used with CBOR.")
|
||||
void cancelActionCbor() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
testdataFactory.createTarget();
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
// check that we can get the cancel action as CBOR
|
||||
final 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)).andReturn().getResponse()
|
||||
.getContentAsByteArray();
|
||||
assertThat(JsonPathUtils.<String> evaluate(cborToJson(result), "$.id"))
|
||||
.isEqualTo(String.valueOf(cancelAction.getId()));
|
||||
assertThat(JsonPathUtils.<String> 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(getJsonProceedingCancelActionFeedback()))
|
||||
.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.")
|
||||
void rootRsCancelActionButContinueAnyway() throws Exception {
|
||||
// prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
implicitLock(ds);
|
||||
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
// controller rejects cancellation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonRejectedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
|
||||
// get update action anyway
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/" + actionId,
|
||||
tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId))))
|
||||
.andExpect(jsonPath("$.deployment.download", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.update", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].version",
|
||||
contains(ds.findFirstModuleByType(runtimeType).get().getVersion())))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].version",
|
||||
contains(ds.findFirstModuleByType(osType).get().getVersion())))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].version",
|
||||
contains(ds.findFirstModuleByType(appType).get().getVersion())));
|
||||
|
||||
// and finish it
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/"
|
||||
+ actionId + "/feedback", tenantAware.getCurrentTenant()).content(
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("message")))
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
// check database after test
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds);
|
||||
assertThat(deploymentManagement.getInstalledDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds);
|
||||
assertThat(
|
||||
targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getInstallationDate())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test for cancel operation of a update action.")
|
||||
void rootRsCancelAction() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final long timeBeforeFirstPoll = System.currentTimeMillis();
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controller}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID).accept(MediaTypes.HAL_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/" + actionId)));
|
||||
final long timeAfterFirstPoll = System.currentTimeMillis() + 1;
|
||||
assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isBetween(timeBeforeFirstPoll, timeAfterFirstPoll);
|
||||
|
||||
// Retrieved is reported
|
||||
|
||||
List<Action> activeActionsByTarget = deploymentManagement
|
||||
.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()).getContent();
|
||||
|
||||
assertThat(activeActionsByTarget).hasSize(1);
|
||||
assertThat(activeActionsByTarget.get(0).getStatus()).isEqualTo(Status.RUNNING);
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent();
|
||||
|
||||
// the canceled action should still be active!
|
||||
assertThat(cancelAction.isActive()).isTrue();
|
||||
assertThat(activeActionsByTarget).hasSize(1);
|
||||
assertThat(activeActionsByTarget.get(0).getStatus()).isEqualTo(Status.CANCELING);
|
||||
|
||||
final long timeBefore2ndPoll = System.currentTimeMillis();
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controller}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.cancelAction.href",
|
||||
equalTo("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + cancelAction.getId())));
|
||||
final long timeAfter2ndPoll = System.currentTimeMillis() + 1;
|
||||
assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isBetween(timeBefore2ndPoll, timeAfter2ndPoll);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId))));
|
||||
assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
|
||||
// controller confirmed cancelled action, should not be active anymore
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON)
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent();
|
||||
assertThat(activeActionsByTarget).isEmpty();
|
||||
final Action canceledAction = deploymentManagement.findAction(cancelAction.getId()).get();
|
||||
assertThat(canceledAction.isActive()).isFalse();
|
||||
assertThat(canceledAction.getStatus()).isEqualTo(Status.CANCELED);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests various bad requests and if the server handles them as expected.")
|
||||
void badCancelAction() throws Exception {
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/1",
|
||||
tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/1",
|
||||
tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/1",
|
||||
tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(get("/{tenant}/controller/v1/34534543/cancelAction/1", tenantAware.getCurrentTenant())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
createCancelAction("34534543");
|
||||
|
||||
// wrong media type
|
||||
mvc.perform(get("/{tenant}/controller/v1/34534543/cancelAction/1", tenantAware.getCurrentTenant())
|
||||
.accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotAcceptable());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the feedback channel of the cancel operation.")
|
||||
void rootRsCancelActionFeedback() throws Exception {
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
|
||||
// cancel action manually
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonProceedingCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
assertThat(countActionStatusAll()).isEqualTo(3);
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonResumedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
assertThat(countActionStatusAll()).isEqualTo(4);
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonScheduledCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(5);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
// cancellation canceled -> should remove the action from active
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonCanceledCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(6);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
// cancellation rejected -> action still active until controller close
|
||||
// it
|
||||
// with finished or
|
||||
// error
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonRejectedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(7);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
// update closed -> should remove the action from active
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(8);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the feeback chanel of for multiple open cancel operations on the same target.")
|
||||
void multipleCancelActionFeedback() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final DistributionSet ds3 = testdataFactory.createDistributionSet("3", true);
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds);
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds2);
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds3.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds3);
|
||||
|
||||
assertThat(countActionStatusAll()).isEqualTo(3);
|
||||
|
||||
// 3 update actions, 0 cancel actions
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(3);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(3);
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
final Action cancelAction2 = deploymentManagement.cancelAction(actionId2);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(3);
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId))));
|
||||
assertThat(countActionStatusAll()).isEqualTo(6);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.cancelAction.href",
|
||||
equalTo("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + cancelAction.getId())));
|
||||
|
||||
// now lets return feedback for the first cancelation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(7);
|
||||
|
||||
// 1 update actions, 1 cancel actions
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction2.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction2.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId2))));
|
||||
assertThat(countActionStatusAll()).isEqualTo(8);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controller}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.cancelAction.href",
|
||||
equalTo("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + cancelAction2.getId())));
|
||||
|
||||
// now lets return feedback for the second cancelation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction2.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(9);
|
||||
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds3);
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/" + actionId3,
|
||||
tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(10);
|
||||
|
||||
// 1 update actions, 0 cancel actions
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action cancelAction3 = deploymentManagement.cancelAction(actionId3);
|
||||
|
||||
// action is in cancelling state
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds3);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction3.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction3.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId3))));
|
||||
assertThat(countActionStatusAll()).isEqualTo(12);
|
||||
|
||||
// now lets return feedback for the third cancelation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction3.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(13);
|
||||
|
||||
// final status
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the feeback channel closing for too many feedbacks, i.e. denial of service prevention.")
|
||||
void tooMuchCancelActionFeedback() throws Exception {
|
||||
testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
final String feedback = getJsonProceedingCancelActionFeedback();
|
||||
// assignDistributionSet creates an ActionStatus and cancel action
|
||||
// stores an action status, so
|
||||
// only 97 action status left
|
||||
for (int i = 0; i < 98; i++) {
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(feedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(feedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("test the correct rejection of various invalid feedback requests")
|
||||
void badCancelActionFeedback() throws Exception {
|
||||
final Action cancelAction = createCancelAction(TestdataFactory.DEFAULT_CONTROLLER_ID);
|
||||
createCancelAction("4715");
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(put("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// bad content type
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_ATOM_XML)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isUnsupportedMediaType());
|
||||
|
||||
// bad body
|
||||
String invalidFeedback = "{\"status\":{\"execution\":\"546456456\",\"result\":{\"finished\":\"none\",\"progress\":{\"cnt\":2,\"of\":5}},\"details\":\"none\"]}}";
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(invalidFeedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(post("/{tenant}/controller/v1/12345/cancelAction/" + cancelAction.getId() + "/feedback",
|
||||
tenantAware.getCurrentTenant()).content(getJsonClosedCancelActionFeedback())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// invalid action
|
||||
invalidFeedback = "{\"id\":\"sdfsdfsdfs\",\"status\":{\"execution\":\"closed\",\"result\":{\"finished\":\"none\",\"progress\":{\"cnt\":2,\"of\":5}},\"details\":\"details\"]}}";
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(invalidFeedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
|
||||
// finaly, get it right :)
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
private Action createCancelAction(final String targetid) {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet(targetid);
|
||||
final Target savedTarget = testdataFactory.createTarget(targetid);
|
||||
final List<Target> toAssign = new ArrayList<>();
|
||||
toAssign.add(savedTarget);
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, toAssign));
|
||||
|
||||
return deploymentManagement.cancelAction(actionId);
|
||||
}
|
||||
|
||||
private long countActionStatusAll() {
|
||||
return actionStatusRepository.count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_KEY_TOO_LONG;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_KEY_VALID;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_VALUE_TOO_LONG;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_VALUE_VALID;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Step;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.rest.util.JsonBuilder;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
/**
|
||||
* Test config data from the controller.
|
||||
*/
|
||||
@ActiveProfiles({ "im", "test" })
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Config Data Resource")
|
||||
class DdiConfigDataTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
public static final String TARGET1_ID = "4717";
|
||||
public static final String TARGET1_CONFIGDATA_PATH = "/{tenant}/controller/v1/" + TARGET1_ID + "/configData";
|
||||
public static final String TARGET2_ID = "4718";
|
||||
public static final String TARGET2_CONFIGDATA_PATH = "/{tenant}/controller/v1/" + TARGET2_ID + "/configData";
|
||||
|
||||
@Test
|
||||
@Description("Verify that config data can be uploaded as CBOR")
|
||||
void putConfigDataAsCbor() throws Exception {
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(ATTRIBUTE_KEY_VALID, ATTRIBUTE_VALUE_VALID);
|
||||
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(jsonToCbor(JsonBuilder.configData(attributes).toString()))
|
||||
.contentType(DdiRestConstants.MEDIA_TYPE_CBOR)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
assertThat(targetManagement.getControllerAttributes(TARGET1_ID)).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.")
|
||||
@SuppressWarnings("squid:S2925")
|
||||
void requestConfigDataIfEmpty() throws Exception {
|
||||
final Target savedTarget = testdataFactory.createTarget("4712");
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant()).accept(MediaTypes.HAL_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.configData.href", equalTo(
|
||||
"http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/4712/configData")));
|
||||
Thread.sleep(1); // is required: otherwise processing the next line is
|
||||
// often too fast and
|
||||
// the following assert will fail
|
||||
assertThat(targetManagement.getByControllerID("4712").orElseThrow(NoSuchElementException::new)
|
||||
.getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(targetManagement.getByControllerID("4712").orElseThrow(NoSuchElementException::new)
|
||||
.getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>(1);
|
||||
attributes.put("dsafsdf", "sdsds");
|
||||
|
||||
final Target updateControllerAttributes = controllerManagement
|
||||
.updateControllerAttributes(savedTarget.getControllerId(), attributes, null);
|
||||
// request controller attributes need to be false because we don't want
|
||||
// to request the controller attributes again
|
||||
assertThat(updateControllerAttributes.isRequestControllerAttributes()).isFalse();
|
||||
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant()).accept(MediaTypes.HAL_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.configData.href").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) "
|
||||
+ "can be uploaded correctly by the controller.")
|
||||
void putConfigData() throws Exception {
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
// initial
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(ATTRIBUTE_KEY_VALID, ATTRIBUTE_VALUE_VALID);
|
||||
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(targetManagement.getControllerAttributes(TARGET1_ID)).isEqualTo(attributes);
|
||||
|
||||
// update
|
||||
attributes.put("sdsds", "123412");
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(targetManagement.getControllerAttributes(TARGET1_ID)).isEqualTo(attributes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) "
|
||||
+ "upload quota is enforced to protect the server from malicious attempts.")
|
||||
void putTooMuchConfigData() throws Exception {
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
// initial
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
for (int i = 0; i < quotaManagement.getMaxAttributeEntriesPerTarget(); i++) {
|
||||
attributes.put("dsafsdf" + i, "sdsds" + i);
|
||||
}
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
attributes = new HashMap<>();
|
||||
attributes.put("on too many", "sdsds");
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(AssignmentQuotaExceededException.class.getName())))
|
||||
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey())));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) "
|
||||
+ "resource behaves as expected in case of invalid request attempts.")
|
||||
void badConfigData() throws Exception {
|
||||
testdataFactory.createTarget("4712");
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).//
|
||||
andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// bad content type
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("dsafsdf", "sdsds");
|
||||
mvc.perform(put("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaTypes.HAL_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isUnsupportedMediaType());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(put("/{tenant}/controller/v1/456456/configData", tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// bad body
|
||||
mvc.perform(put("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())
|
||||
.content("{\"id\": \"51659181\"}").contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that invalid config data attributes are handled correctly.")
|
||||
void putConfigDataWithInvalidAttributes() throws Exception {
|
||||
// create a target
|
||||
testdataFactory.createTarget(TARGET2_ID);
|
||||
putAndVerifyConfigDataWithKeyTooLong();
|
||||
putAndVerifyConfigDataWithValueTooLong();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that config data (device attributes) can be updated by the controller using different update modes (merge, replace, remove).")
|
||||
void putConfigDataWithDifferentUpdateModes() throws Exception {
|
||||
|
||||
// create a target
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
// no update mode
|
||||
putConfigDataWithoutUpdateMode();
|
||||
|
||||
// update mode REPLACE
|
||||
putConfigDataWithUpdateModeReplace();
|
||||
|
||||
// update mode MERGE
|
||||
putConfigDataWithUpdateModeMerge();
|
||||
|
||||
// update mode REMOVE
|
||||
putConfigDataWithUpdateModeRemove();
|
||||
|
||||
// invalid update mode
|
||||
putConfigDataWithInvalidUpdateMode();
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putAndVerifyConfigDataWithKeyTooLong() throws Exception {
|
||||
final Map<String, String> attributes = Collections.singletonMap(ATTRIBUTE_KEY_TOO_LONG, ATTRIBUTE_VALUE_VALID);
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET2_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(InvalidTargetAttributeException.class.getName())))
|
||||
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_TARGET_ATTRIBUTES_INVALID.getKey())));
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putAndVerifyConfigDataWithValueTooLong() throws Exception {
|
||||
final Map<String, String> attributes = Collections.singletonMap(ATTRIBUTE_KEY_VALID, ATTRIBUTE_VALUE_TOO_LONG);
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET2_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(InvalidTargetAttributeException.class.getName())))
|
||||
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_TARGET_ATTRIBUTES_INVALID.getKey())));
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithInvalidUpdateMode() throws Exception {
|
||||
// create some attriutes
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("k0", "v0");
|
||||
attributes.put("k1", "v1");
|
||||
|
||||
// use an invalid update mode
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes, "KJHGKJHGKJHG").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithUpdateModeRemove() throws Exception {
|
||||
// get the current attributes
|
||||
final int previousSize = targetManagement.getControllerAttributes(DdiConfigDataTest.TARGET1_ID).size();
|
||||
|
||||
// update the attributes using update mode REMOVE
|
||||
final Map<String, String> removeAttributes = new HashMap<>();
|
||||
removeAttributes.put("k1", "foo");
|
||||
removeAttributes.put("k3", "bar");
|
||||
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(removeAttributes, "remove").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// verify attribute removal
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).hasSize(previousSize - 2);
|
||||
assertThat(updatedAttributes).doesNotContainKeys("k1", "k3");
|
||||
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithUpdateModeMerge()
|
||||
throws Exception {
|
||||
// get the current attributes
|
||||
final Map<String, String> attributes = new HashMap<>(
|
||||
targetManagement.getControllerAttributes(DdiConfigDataTest.TARGET1_ID));
|
||||
|
||||
// update the attributes using update mode MERGE
|
||||
final Map<String, String> mergeAttributes = new HashMap<>();
|
||||
mergeAttributes.put("k1", "v1_modified_again");
|
||||
mergeAttributes.put("k4", "v4");
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(mergeAttributes, "merge").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// verify attribute merge
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).hasSize(4);
|
||||
assertThat(updatedAttributes).containsAllEntriesOf(mergeAttributes);
|
||||
assertThat(updatedAttributes).containsEntry("k1", "v1_modified_again");
|
||||
attributes.keySet().forEach(assertThat(updatedAttributes)::containsKey);
|
||||
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithUpdateModeReplace()
|
||||
throws Exception {
|
||||
// get the current attributes
|
||||
final Map<String, String> attributes = new HashMap<>(
|
||||
targetManagement.getControllerAttributes(DdiConfigDataTest.TARGET1_ID));
|
||||
|
||||
// update the attributes using update mode REPLACE
|
||||
final Map<String, String> replacementAttributes = new HashMap<>();
|
||||
replacementAttributes.put("k1", "v1_modified");
|
||||
replacementAttributes.put("k2", "v2");
|
||||
replacementAttributes.put("k3", "v3");
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(replacementAttributes, "replace").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// verify attribute replacement
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).hasSize(replacementAttributes.size());
|
||||
assertThat(updatedAttributes).containsAllEntriesOf(replacementAttributes);
|
||||
assertThat(updatedAttributes).containsEntry("k1", "v1_modified");
|
||||
attributes.entrySet().forEach(assertThat(updatedAttributes)::doesNotContain);
|
||||
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithoutUpdateMode()
|
||||
throws Exception {
|
||||
// create some attriutes
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("k0", "v0");
|
||||
attributes.put("k1", "v1");
|
||||
|
||||
// set the initial attributes
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
// verify the initial parameters
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).containsExactlyEntriesOf(attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.repository.jpa.management.JpaConfirmationManagement.CONFIRMATION_CODE_MSG_PREFIX;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Step;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
/**
|
||||
* Test confirmation base from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Confirmation Action Resource")
|
||||
class DdiConfirmationBaseTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String DEFAULT_CONTROLLER_ID = "4747";
|
||||
|
||||
@Test
|
||||
@Description("Forced deployment to a controller. Checks if the confirmation resource response payload for a given deployment is as expected.")
|
||||
void verifyConfirmationReferencesInControllerBase(@Autowired ActionStatusRepository actionStatusRepository) throws Exception {
|
||||
enableConfirmationFlow();
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget(DdiConfirmationBaseTest.DEFAULT_CONTROLLER_ID);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
|
||||
final List<Target> targetsAssignedToDs = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
Action.ActionType.FORCED).getAssignedEntity().stream().map(Action::getTarget)
|
||||
.collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
|
||||
assignDistributionSet(ds2, targetsAssignedToDs).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
final long current = System.currentTimeMillis();
|
||||
final String expectedConfirmationBaseLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||
tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, uaction.getId());
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href",
|
||||
containsString(expectedConfirmationBaseLink)))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(actionStatusRepository.count()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyConfirmationBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact,
|
||||
artifactSignature, action.getId(),
|
||||
findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced", "forced");
|
||||
|
||||
// Retrieved is reported
|
||||
final Iterable<ActionStatus> actionStatus = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Sort.Direction.DESC, "id"), uaction.getId());
|
||||
assertThat(actionStatus).hasSize(1)
|
||||
.allMatch(status -> status.getStatus() == Action.Status.WAIT_FOR_CONFIRMATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the deployment resource is available as CBOR")
|
||||
void confirmationResourceCbor() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
assignDistributionSet(distributionSet.getId(), target.getName());
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
// get confirmation base
|
||||
performGet(CONFIRMATION_BASE_ACTION, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(), action.getId().toString());
|
||||
|
||||
// get artifacts
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
String.valueOf(softwareModuleId));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the confirmation endpoint is not available.")
|
||||
void confirmationEndpointNotExposed() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").doesNotExist());
|
||||
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the deploymentBase endpoint is not available for action ins WFC state.")
|
||||
void deploymentEndpointNotAccessibleForActionsWFC() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the confirmation endpoints are still available after deactivating the confirmation flow.")
|
||||
void verifyConfirmationBaseEndpointsArePresentAfterDisablingConfirmationFlow() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
// disable confirmation flow
|
||||
disableConfirmationFlow();
|
||||
|
||||
// confirmation base should still be exposed
|
||||
verifyActionInConfirmationBaseState(savedTarget.getControllerId(), savedAction.getId());
|
||||
|
||||
// verify confirmation endpoint is still accessible
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.DENIED, 20,
|
||||
"Action denied message.").andExpect(status().isOk());
|
||||
|
||||
// confirmation base should still be exposed
|
||||
verifyActionInConfirmationBaseState(savedTarget.getControllerId(), savedAction.getId());
|
||||
|
||||
// verify confirmation endpoint is still accessible
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED, 10,
|
||||
"Action confirmed message.").andExpect(status().isOk());
|
||||
|
||||
// assert deployment link is exposed to the target
|
||||
verifyActionInDeploymentBaseState(controllerId, savedAction.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller sends a confirmed action state.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void sendConfirmedActionStateFeedbackTest() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED, 10,
|
||||
"Action confirmed message.").andExpect(status().isOk());
|
||||
|
||||
// assert deployment link is exposed to the target
|
||||
verifyActionInDeploymentBaseState(controllerId, savedAction.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Confirmation base provides right values if auto-confirm not active.")
|
||||
void getConfirmationBaseProvidesAutoConfirmStatusNotActive() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final String controllerId = testdataFactory.createTarget("989").getControllerId();
|
||||
assignDistributionSet(testdataFactory.createDistributionSet("").getId(), controllerId);
|
||||
final long actionId = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent().get(0)
|
||||
.getId();
|
||||
|
||||
final String confirmationBaseActionLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId);
|
||||
|
||||
final String activateAutoConfLink = String.format("/%s/controller/v1/%s/confirmationBase/activateAutoConfirm",
|
||||
tenantAware.getCurrentTenant(), controllerId);
|
||||
|
||||
mvc.perform(
|
||||
get(CONFIRMATION_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("autoConfirm.active", equalTo(Boolean.FALSE)))
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href", containsString(confirmationBaseActionLink)))
|
||||
.andExpect(jsonPath("$._links.activateAutoConfirm.href", containsString(activateAutoConfLink)))
|
||||
.andExpect(jsonPath("$._links.deactivateAutoConfirm").doesNotExist());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("possibleActiveStates")
|
||||
@Description("Confirmation base provides right values if auto-confirm is active.")
|
||||
void getConfirmationBaseProvidesAutoConfirmStatusActive(final String initiator, final String remark)
|
||||
throws Exception {
|
||||
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||
|
||||
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||
|
||||
final String deactivateAutoConfLink = String.format(
|
||||
"/%s/controller/v1/%s/confirmationBase/deactivateAutoConfirm", tenantAware.getCurrentTenant(),
|
||||
controllerId);
|
||||
|
||||
mvc.perform(
|
||||
get(CONFIRMATION_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("autoConfirm.active", equalTo(Boolean.TRUE)))
|
||||
.andExpect(initiator == null ? jsonPath("autoConfirm.initiator").doesNotExist()
|
||||
: jsonPath("autoConfirm.initiator", equalTo(initiator)))
|
||||
.andExpect(remark == null ? jsonPath("autoConfirm.remark").doesNotExist()
|
||||
: jsonPath("autoConfirm.remark", equalTo(remark)))
|
||||
.andExpect(jsonPath("$._links.deactivateAutoConfirm.href", containsString(deactivateAutoConfLink)))
|
||||
.andExpect(jsonPath("$._links.activateAutoConfirm").doesNotExist());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("possibleActiveStates")
|
||||
@Description("Verify auto-confirm activation is handled correctly.")
|
||||
void activateAutoConfirmation(final String initiator, final String remark) throws Exception {
|
||||
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||
|
||||
final DdiActivateAutoConfirmation body = new DdiActivateAutoConfirmation(initiator, remark);
|
||||
|
||||
mvc.perform(post(ACTIVATE_AUTO_CONFIRM, tenantAware.getCurrentTenant(), controllerId)
|
||||
.content(getMapper().writeValueAsString(body)).contentType(MediaType.APPLICATION_JSON_UTF8))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
assertThat(confirmationManagement.getStatus(controllerId)).hasValueSatisfying(status -> {
|
||||
assertThat(status.getInitiator()).isEqualTo(initiator);
|
||||
assertThat(status.getRemark()).isEqualTo(remark);
|
||||
assertThat(status.getCreatedBy()).isEqualTo("bumlux");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify auto-confirm deactivation is handled correctly.")
|
||||
void deactivateAutoConfirmation() throws Exception {
|
||||
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||
|
||||
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
|
||||
|
||||
mvc.perform(post(DEACTIVATE_AUTO_CONFIRM, tenantAware.getCurrentTenant(), controllerId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
assertThat(confirmationManagement.getStatus(controllerId)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller sends a denied action state.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||
@Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void sendDeniedActionStateFeedbackTest() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("989");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
String controllerId = savedTarget.getControllerId();
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.DENIED, 10,
|
||||
"Action denied message.").andExpect(status().isOk());
|
||||
|
||||
// asserts that deployment link is not available
|
||||
final String expectedConfirmationBaseLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||
tenantAware.getCurrentTenant(), controllerId, savedAction.getId());
|
||||
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href", containsString(expectedConfirmationBaseLink)));
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void testActionHistoryCount() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("990");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
final String CONFIRMED_MESSAGE = "Action confirmed message.";
|
||||
final Integer CONFIRMED_CODE = 10;
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED,
|
||||
CONFIRMED_CODE, CONFIRMED_MESSAGE).andExpect(status().isOk());
|
||||
|
||||
// confirmationBase not available in RUNNING state anymore
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), savedTarget.getControllerId(),
|
||||
savedAction.getId()).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// assert confirmed message against deploymentBase endpoint
|
||||
// this call will update the action due to retrieved action status update
|
||||
mvc.perform(
|
||||
get(DEPLOYMENT_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), savedTarget.getControllerId(),
|
||||
savedAction.getId()).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(CONFIRMED_MESSAGE))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
hasItem(containsString(String.format(CONFIRMATION_CODE_MSG_PREFIX, CONFIRMED_CODE)))));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> possibleActiveStates() {
|
||||
return Stream.of(Arguments.of("someInitiator", "someRemark"), Arguments.of(null, "someRemark"),
|
||||
Arguments.of("someInitiator", null), Arguments.of(null, null));
|
||||
}
|
||||
|
||||
@Step
|
||||
private void verifyActionInDeploymentBaseState(final String controllerId, final long actionId) throws Exception {
|
||||
final String expectedDeploymentBaseLink = String.format("/%s/controller/v1/%s/deploymentBase/%d",
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId);
|
||||
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink)))
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").doesNotExist());
|
||||
|
||||
// assert that deployment endpoint is working
|
||||
mvc.perform(get(expectedDeploymentBaseLink).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void verifyActionInConfirmationBaseState(final String controllerId, final long actionId) throws Exception {
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
private ResultActions sendConfirmationFeedback(final Target target, final Action action,
|
||||
final DdiConfirmationFeedback.Confirmation confirmation, Integer code, String message) throws Exception {
|
||||
|
||||
if (message == null) {
|
||||
message = RandomStringUtils.randomAlphanumeric(1000);
|
||||
}
|
||||
|
||||
final String feedback = getJsonConfirmationFeedback(confirmation, code, Collections.singletonList(message));
|
||||
return mvc.perform(
|
||||
post(CONFIRMATION_FEEDBACK, tenantAware.getCurrentTenant(), target.getControllerId(), action.getId())
|
||||
.content(feedback).contentType(MediaType.APPLICATION_JSON));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,838 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
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;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.rest.exception.MessageNotReadableException;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
/**
|
||||
* Test deployment base from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Deployment Action Resource")
|
||||
class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String DEFAULT_CONTROLLER_ID = "4712";
|
||||
|
||||
@Autowired
|
||||
private ActionRepository actionRepository;
|
||||
@Autowired
|
||||
private ActionStatusRepository actionStatusRepository;
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the deployment resource is available as CBOR")
|
||||
void deploymentResourceCbor() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
assignDistributionSet(distributionSet.getId(), target.getName());
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
// get deployment base
|
||||
performGet(DEPLOYMENT_BASE, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), action.getId().toString());
|
||||
|
||||
// get artifacts
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
String.valueOf(softwareModuleId));
|
||||
|
||||
final byte[] feedback = jsonToCbor(getJsonProceedingDeploymentActionFeedback());
|
||||
|
||||
postDeploymentFeedback(MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), target.getControllerId(),
|
||||
action.getId(), feedback, status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that artifacts are not found, when software module does not exists.")
|
||||
void artifactsNotFound() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isNotFound(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), "1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that artifacts are found, when software module exists.")
|
||||
void artifactsExists() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
assignDistributionSet(distributionSet.getId(), target.getName());
|
||||
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId.toString())
|
||||
.andExpect(jsonPath("$", hasSize(3)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename0')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename1')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename2')]", hasSize(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Forced deployment to a controller. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentForceAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> targetsAssignedToDs = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
ActionType.FORCED).getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
|
||||
assignDistributionSet(ds2, targetsAssignedToDs).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact,
|
||||
artifactSignature, action.getId(),
|
||||
findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced", "forced");
|
||||
|
||||
// Retrieved is reported
|
||||
final Iterable<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId());
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Checks that the deploymentBase URL changes when the action is switched from soft to forced in TIMEFORCED case.")
|
||||
void changeEtagIfActionSwitchesFromSoftToForced() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds.getId(), target.getControllerId(),
|
||||
ActionType.TIMEFORCED, System.currentTimeMillis() + 2_000));
|
||||
|
||||
MvcResult mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID).andReturn();
|
||||
|
||||
final String urlBeforeSwitch = JsonPath.compile("_links.deploymentBase.href")
|
||||
.read(mvcResult.getResponse().getContentAsString()).toString();
|
||||
|
||||
// Time is not yet over, so we should see the same URL
|
||||
mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andReturn();
|
||||
assertThat(JsonPath.compile("_links.deploymentBase.href").read(mvcResult.getResponse().getContentAsString())
|
||||
.toString()).isEqualTo(urlBeforeSwitch)
|
||||
.startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, actionId.toString()));
|
||||
|
||||
// After the time is over we should see a new etag
|
||||
TimeUnit.MILLISECONDS.sleep(2_000);
|
||||
|
||||
mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andReturn();
|
||||
|
||||
assertThat(JsonPath.compile("_links.deploymentBase.href").read(mvcResult.getResponse().getContentAsString())
|
||||
.toString()).isNotEqualTo(urlBeforeSwitch);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Attempt/soft deployment to a controller. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentAttemptAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final String visibleMetadataOsKey = "metaDataVisible";
|
||||
final String visibleMetadataOsValue = "withValue";
|
||||
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key(visibleMetadataOsKey).value(visibleMetadataOsValue).targetVisible(true));
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key("metaDataNotVisible").value("withValue").targetVisible(false));
|
||||
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), ActionType.SOFT)
|
||||
.getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
assignDistributionSet(ds2, saved).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, visibleMetadataOsKey,
|
||||
visibleMetadataOsValue, artifact, artifactSignature, action.getId(), "attempt", "attempt",
|
||||
getOsModule(findDistributionSetByAction));
|
||||
|
||||
// Retrieved is reported
|
||||
final List<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId()).getContent();
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Attempt/soft deployment to a controller including automated switch to hard. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentAutoForceAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
ActionType.TIMEFORCED).getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
assignDistributionSet(ds2, saved).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact,
|
||||
artifactSignature, action.getId(),
|
||||
findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced", "forced");
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaTypes.HAL_JSON, ds, artifact, artifactSignature,
|
||||
action.getId(), findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced",
|
||||
"forced");
|
||||
|
||||
// Retrieved is reported
|
||||
final Iterable<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId()).getContent();
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download-only (forced + skip) deployment to a controller. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentDownloadOnlyAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key("metaDataVisible").value("withValue").targetVisible(true));
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key("metaDataNotVisible").value("withValue").targetVisible(false));
|
||||
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
ActionType.DOWNLOAD_ONLY).getAssignedEntity().stream().map(Action::getTarget)
|
||||
.collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
assignDistributionSet(ds2, saved).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, "metaDataVisible",
|
||||
"withValue", artifact, artifactSignature, action.getId(), "forced", "skip",
|
||||
getOsModule(findDistributionSetByAction));
|
||||
|
||||
// Retrieved is reported
|
||||
final List<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId()).getContent();
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test various invalid access attempts to the deployment resource und the expected behaviour of the server.")
|
||||
void badDeploymentAction() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "not-existing", "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// no deployment
|
||||
mvc.perform(
|
||||
MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// wrong media type
|
||||
final List<Target> toAssign = Collections.singletonList(target);
|
||||
final DistributionSet savedSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(savedSet, toAssign));
|
||||
mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID,
|
||||
actionId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
mvc.perform(MockMvcRequestBuilders
|
||||
.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, actionId)
|
||||
.accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotAcceptable());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("The server protects itself against to many feedback upload attempts. The test verifies that "
|
||||
+ "it is not possible to exceed the configured maximum number of feedback uploads.")
|
||||
void tooMuchDeploymentActionFeedback() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), DEFAULT_CONTROLLER_ID);
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
|
||||
final String feedback = getJsonProceedingDeploymentActionFeedback();
|
||||
// assign distribution set creates an action status, so only 99 left
|
||||
for (int i = 0; i < 99; i++) {
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isOk());
|
||||
}
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("The server protects itself against too large feedback bodies. The test verifies that "
|
||||
+ "it is not possible to exceed the configured maximum number of feedback details.")
|
||||
void tooMuchDeploymentActionMessagesInFeedback() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), DEFAULT_CONTROLLER_ID);
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
|
||||
final List<String> messages = new ArrayList<>();
|
||||
for (int i = 0; i < quotaManagement.getMaxMessagesPerActionStatus() + 1; i++) {
|
||||
messages.add(String.valueOf(i));
|
||||
}
|
||||
|
||||
final String feedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
|
||||
null, messages);
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Multiple uploads of deployment status feedback to the server.")
|
||||
void multipleDeploymentActionFeedback() throws Exception {
|
||||
testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
testdataFactory.createTarget("4713");
|
||||
testdataFactory.createTarget("4714");
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final DistributionSet ds3 = testdataFactory.createDistributionSet("3", true);
|
||||
|
||||
final Long actionId1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds1);
|
||||
final Long actionId2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds2);
|
||||
final Long actionId3 = getFirstAssignedActionId(assignDistributionSet(ds3.getId(), DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds3);
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 3, Optional.empty());
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.UNKNOWN)).hasSize(2);
|
||||
|
||||
// action1 done
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId1, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 2,
|
||||
Optional.of(ds1));
|
||||
assertStatusMessagesCount(4);
|
||||
|
||||
// action2 done
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId2, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 1,
|
||||
Optional.of(ds2));
|
||||
assertStatusMessagesCount(5);
|
||||
|
||||
// action3 done
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId3, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.IN_SYNC, 0,
|
||||
Optional.of(ds3));
|
||||
assertStatusMessagesCount(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that an update action is correctly set to error if the controller provides error feedback.")
|
||||
void rootRsSingleDeploymentActionWithErrorFeedback() throws Exception {
|
||||
DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Target savedTarget = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus())
|
||||
.isEqualTo(TargetUpdateStatus.UNKNOWN);
|
||||
assignDistributionSet(ds, Collections.singletonList(savedTarget));
|
||||
final Action action = actionRepository
|
||||
.findAll(ActionSpecifications.byDistributionSetId(ds.getId()), PAGE)
|
||||
.map(Action.class::cast).getContent().get(0);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.FAILURE,
|
||||
Collections.singletonList("Error message")),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.empty(), TargetUpdateStatus.ERROR, 0, Optional.empty());
|
||||
assertThat(deploymentManagement.countActionsByTarget(DEFAULT_CONTROLLER_ID)).isEqualTo(1);
|
||||
assertTargetCountByStatus(0, 1, 0);
|
||||
|
||||
// redo
|
||||
ds = distributionSetManagement.getWithDetails(ds.getId()).get();
|
||||
assignDistributionSet(ds,
|
||||
Collections.singletonList(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get()));
|
||||
final Action action2 = deploymentManagement.findActiveActionsByTarget(PAGE, DEFAULT_CONTROLLER_ID).getContent()
|
||||
.get(0);
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action2.getId(), getJsonClosedCancelActionFeedback(),
|
||||
status().isOk());
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds), TargetUpdateStatus.IN_SYNC, 0, Optional.of(ds));
|
||||
assertTargetCountByStatus(0, 0, 1);
|
||||
assertThat(deploymentManagement.findInActiveActionsByTarget(PAGE, DEFAULT_CONTROLLER_ID)).hasSize(2);
|
||||
assertThat(countActionStatusAll()).isEqualTo(4);
|
||||
assertThat(deploymentManagement.findActionStatusByAction(PAGE, action.getId()).getContent()).haveAtLeast(1,
|
||||
new ActionStatusCondition(Status.ERROR));
|
||||
assertThat(deploymentManagement.findActionStatusByAction(PAGE, action2.getId()).getContent()).haveAtLeast(1,
|
||||
new ActionStatusCondition(Status.FINISHED));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that the controller can provided as much feedback entries as necessary as long as it is in the configured limits.")
|
||||
void rootRsSingleDeploymentActionFeedback() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds,
|
||||
Collections.singletonList(testdataFactory.createTarget(DEFAULT_CONTROLLER_ID))));
|
||||
implicitLock(ds);
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds), TargetUpdateStatus.PENDING, 1, Optional.empty());
|
||||
|
||||
// Now valid Feedback
|
||||
for (int i = 0; i < 4; i++) {
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonProceedingDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertActionStatusCount(i + 2, i);
|
||||
|
||||
}
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonScheduledDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertActionStatusCount(6, 5);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonResumedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertActionStatusCount(7, 6);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonCanceledDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertStatusAndActiveActionsCount(TargetUpdateStatus.PENDING, 1);
|
||||
assertActionStatusCount(8, 7, 0, 0, 1);
|
||||
assertTargetCountByStatus(1, 0, 0);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonRejectedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertStatusAndActiveActionsCount(TargetUpdateStatus.PENDING, 1);
|
||||
assertActionStatusCount(9, 6, 1, 0, 1);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertStatusAndActiveActionsCount(TargetUpdateStatus.IN_SYNC, 0);
|
||||
assertActionStatusCount(10, 7, 1, 1, 1);
|
||||
assertTargetCountByStatus(0, 0, 1);
|
||||
|
||||
assertThat(targetManagement.findByInstalledDistributionSet(PAGE, ds.getId())).hasSize(1);
|
||||
assertThat(targetManagement.findByAssignedDistributionSet(PAGE, ds.getId())).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Various forbidden request attempts on the feedback resource. Ensures correct answering behaviour as expected to these kind of errors.")
|
||||
void badDeploymentActionFeedback() throws Exception {
|
||||
final DistributionSet savedSet = testdataFactory.createDistributionSet("");
|
||||
final DistributionSet savedSet2 = testdataFactory.createDistributionSet("1");
|
||||
|
||||
// target does not exist
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, 1234L, getJsonProceedingDeploymentActionFeedback(),
|
||||
status().isNotFound());
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
|
||||
// Action does not exist
|
||||
postDeploymentFeedback("4713", 1234L, getJsonProceedingDeploymentActionFeedback(), status().isNotFound());
|
||||
|
||||
assignDistributionSet(savedSet, Collections.singletonList(savedTarget)).getAssignedEntity().iterator().next();
|
||||
assignDistributionSet(savedSet2, Collections.singletonList(testdataFactory.createTarget("4713")));
|
||||
|
||||
final Action updateAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
// action exists but is not assigned to this target
|
||||
postDeploymentFeedback("4713", updateAction.getId(), getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING,
|
||||
DdiResult.FinalResult.NONE, Collections.singletonList("")), status().isNotFound());
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID, "2")).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "2"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "2"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that an invalid id in feedback body returns a bad request.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1) })
|
||||
void invalidIdInFeedbackReturnsBadRequest() throws Exception {
|
||||
final Target target = testdataFactory.createTarget("1080");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), "1080");
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
final String invalidFeedback = "{\"id\":\"AAAA\",\"status\":{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\",\"progress\":{\"cnt\":2,\"of\":5}},\"details\":\"details\"]}}";
|
||||
postDeploymentFeedback("1080", action.getId(), invalidFeedback, status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a missing feedback result in feedback body returns a bad request.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1) })
|
||||
void missingResultAttributeInFeedbackReturnsBadRequest() throws Exception {
|
||||
|
||||
final Target target = testdataFactory.createTarget("1080");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), "1080");
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
|
||||
final String missingResultInFeedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, (DdiResult) null,
|
||||
Collections.singletonList("test"));
|
||||
postDeploymentFeedback("1080", action.getId(), missingResultInFeedback, status().isBadRequest())
|
||||
.andExpect(jsonPath("$.*", hasSize(3)))
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(MessageNotReadableException.class.getName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a missing finished result in feedback body returns a bad request.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1) })
|
||||
void missingFinishedAttributeInFeedbackReturnsBadRequest() throws Exception {
|
||||
final Target target = testdataFactory.createTarget("1080");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
assignDistributionSet(ds.getId(), "1080");
|
||||
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
final String missingFinishedResultInFeedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED,
|
||||
new DdiResult(null, null),
|
||||
Collections.singletonList("test"));
|
||||
|
||||
postDeploymentFeedback("1080", action.getId(), missingFinishedResultInFeedback, status().isBadRequest())
|
||||
.andExpect(jsonPath("$.*", hasSize(3)))
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(MessageNotReadableException.class.getName())));
|
||||
}
|
||||
|
||||
private long countActionStatusAll() {
|
||||
return actionStatusRepository.count();
|
||||
}
|
||||
|
||||
private void getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final String visibleMetadataOsKey, final String visibleMetadataOsValue,
|
||||
final Artifact artifact, final Artifact artifactSignature, final Long actionId, final String downloadType,
|
||||
final String updateType, final Long osModuleId) throws Exception {
|
||||
getAndVerifyDeploymentBasePayload(controllerId, mediaType, ds, artifact, artifactSignature, actionId,
|
||||
osModuleId, downloadType, updateType).andExpect(
|
||||
jsonPath("$.deployment.chunks[?(@.part=='os')].metadata[0].key").value(visibleMetadataOsKey))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].metadata[0].value")
|
||||
.value(visibleMetadataOsValue));
|
||||
}
|
||||
|
||||
private void assertActionStatusCount(final int actionStatusCount, final int minActionStatusCountInPage) {
|
||||
final Target target = targetManagement.getByControllerID(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID).get();
|
||||
assertThat(target.getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING);
|
||||
|
||||
assertTargetCountByStatus(1, 0, 0);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())).hasSize(1);
|
||||
assertThat(countActionStatusAll()).isEqualTo(actionStatusCount);
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(minActionStatusCountInPage,
|
||||
new ActionStatusCondition(Status.RUNNING));
|
||||
}
|
||||
|
||||
private Target createTargetAndAssertNoActiveActions() {
|
||||
final Target savedTarget = testdataFactory.createTarget(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
assertThat(deploymentManagement.countActionsAll()).isZero();
|
||||
assertThat(countActionStatusAll()).isZero();
|
||||
return savedTarget;
|
||||
}
|
||||
|
||||
private void assertStatusMessagesCount(final int actionStatusMessagesCount) {
|
||||
final Iterable<ActionStatus> actionStatusMessages;
|
||||
actionStatusMessages = findActionStatusAll(PageRequest.of(0, 100, Direction.DESC, "id"))
|
||||
.getContent();
|
||||
assertThat(actionStatusMessages).hasSize(actionStatusMessagesCount);
|
||||
assertThat(actionStatusMessages).haveAtLeast(1, new ActionStatusCondition(Status.FINISHED));
|
||||
}
|
||||
|
||||
private void findTargetAndAssertUpdateStatus(final Optional<DistributionSet> ds,
|
||||
final TargetUpdateStatus updateStatus, final int activeActions,
|
||||
final Optional<DistributionSet> installedDs) {
|
||||
final Target myT = targetManagement.getByControllerID(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID).get();
|
||||
assertThat(myT.getUpdateStatus()).isEqualTo(updateStatus);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, myT.getControllerId())).hasSize(activeActions);
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(myT.getControllerId())).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.getInstalledDistributionSet(myT.getControllerId())).isEqualTo(installedDs);
|
||||
}
|
||||
|
||||
private void assertTargetCountByStatus(final int pending, final int error, final int inSync) {
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.PENDING))
|
||||
.hasSize(pending);
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.ERROR)).hasSize(error);
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.IN_SYNC))
|
||||
.hasSize(inSync);
|
||||
}
|
||||
|
||||
private void assertActionStatusCount(final int total, final int running, final int warning, final int finished,
|
||||
final int canceled) {
|
||||
assertThat(countActionStatusAll()).isEqualTo(total);
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(running,
|
||||
new ActionStatusCondition(Status.RUNNING));
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(warning,
|
||||
new ActionStatusCondition(Status.WARNING));
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(canceled,
|
||||
new ActionStatusCondition(Status.CANCELED));
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(finished,
|
||||
new ActionStatusCondition(Status.FINISHED));
|
||||
}
|
||||
|
||||
private void assertStatusAndActiveActionsCount(final TargetUpdateStatus status, final int activeActions) {
|
||||
final Target target = targetManagement.getByControllerID(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID).get();
|
||||
assertThat(target.getUpdateStatus()).isEqualTo(status);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()))
|
||||
.hasSize(activeActions);
|
||||
}
|
||||
|
||||
private Page<ActionStatus> findActionStatusAll(final Pageable pageable) {
|
||||
return JpaManagementHelper.findAllWithCountBySpec(actionStatusRepository, pageable, null);
|
||||
}
|
||||
|
||||
private static class ActionStatusCondition extends Condition<ActionStatus> {
|
||||
|
||||
private final Action.Status status;
|
||||
|
||||
public ActionStatusCondition(final Action.Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(final ActionStatus value) {
|
||||
return value.getStatus() == status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
/**
|
||||
* Test installed base from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Installed Base Resource")
|
||||
public class DdiInstalledBaseTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
ActionStatusRepository actionStatusRepository;
|
||||
private static final int ARTIFACT_SIZE = 5 * 1024;
|
||||
private static final String CONTROLLER_ID = "4715";
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the installed base resource is available as CBOR")
|
||||
public void installedBaseResourceCbor() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = ds.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
// get installed base
|
||||
performGet(INSTALLED_BASE, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), actionId.toString());
|
||||
|
||||
// get artifacts
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
String.valueOf(softwareModuleId));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that assigned version is self assigned version")
|
||||
public void installedVersion() throws Exception {
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
// update assigned version
|
||||
putInstalledBase(target.getControllerId(), getJsonInstalledBase(ds.getName(), ds.getVersion()), status()
|
||||
.isCreated());
|
||||
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get().getId())
|
||||
.isEqualTo(ds.getId());
|
||||
|
||||
// update assigned version while version already assigned
|
||||
putInstalledBase(target.getControllerId(), getJsonInstalledBase(ds.getName(), ds.getVersion()), status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that installedVersion is version self assigned")
|
||||
public void installedVersionNotExist() throws Exception {
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final String dsName = "unknown";
|
||||
final String dsVersion = "1.0.0";
|
||||
|
||||
// get installed base
|
||||
putInstalledBase(target.getControllerId(), getJsonInstalledBase(dsName, dsVersion), status().isNotFound());
|
||||
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments to a controller. Checks that action is represented as installedBase after installation.")
|
||||
public void deploymentSeveralActionsInInstalledBase() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact2 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds2), "test2", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature2 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds2), "test2.signature", ARTIFACT_SIZE);
|
||||
|
||||
// Run test with 1st action
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId1.toString()))));
|
||||
|
||||
getAndVerifyDeploymentBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Closed")),
|
||||
status().isOk());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// Run test with 2nd action
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId1.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId2.toString()))));
|
||||
|
||||
getAndVerifyDeploymentBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds2, artifact2, artifactSignature2,
|
||||
actionId2, ds2.findFirstModuleByType(osType).get().getId(), Action.ActionType.FORCED);
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Closed")),
|
||||
status().isOk());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds2, artifact2, artifactSignature2,
|
||||
actionId2, ds2.findFirstModuleByType(osType).get().getId(), Action.ActionType.FORCED);
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId2.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
// older installed action is still accessible, although not part of controller
|
||||
// base
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments of same ds to a controller. Checks that cancelled action in history is not linked as installedBase.")
|
||||
public void deploymentActionsOfSameDsWithCancelledActionInHistory() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
// assign ds1, action1 - and provide cancel feedback
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
deploymentManagement.cancelAction(actionId1);
|
||||
postCancelFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds1, action2 - and provide cancel feedback
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
deploymentManagement.cancelAction(actionId2);
|
||||
postCancelFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds1, action 3 - and provide success feedback
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId3,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// Test: latest succeeded action is returned in installedBase
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId3.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId3, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// cancelled action are not accessible
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId1.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId2.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments of same ds to a controller. Checks that latest cancelled action does not override actual installed ds.")
|
||||
public void deploymentActionsOfSameDsWithCancelledAction() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
|
||||
// assign ds1, action1 - and provide success feedback
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Success")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds2, action2 - assign ds1, action 3 - and cancel both
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
deploymentManagement.cancelAction(actionId2);
|
||||
postCancelFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
deploymentManagement.cancelAction(actionId3);
|
||||
postCancelFeedback(target.getControllerId(), actionId3,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// Test: the succeeded action is returned in installedBase instead of the latest
|
||||
// cancelled action
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId1.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// cancelled action are not accessible
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId2.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId3.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments of same ds to a controller. Checks that latest running action does not override actual installed ds.")
|
||||
public void deploymentActionsOfSameDsWithRunningAction() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
|
||||
// assign ds1, action1 - and provide success feedback
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Success")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds2, action2 - assign ds1, action 3 - and cancel action 2
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
deploymentManagement.cancelAction(actionId2);
|
||||
postCancelFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// Test: the succeeded action is returned in installedBase instead of the latest
|
||||
// cancelled action
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId1.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId3.toString()))));
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// cancelled action are not accessible
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId2.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId3.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test open deployment to a controller. Checks that installedBase returns 404 for a pending action.")
|
||||
public void installedBaseReturns404ForPendingAction() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target));
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId.toString()))));
|
||||
|
||||
performGet(DEPLOYMENT_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId(), actionId.toString());
|
||||
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that artifacts are found, after the action was already closed.")
|
||||
public void artifactsOfInstalledActionExist() throws Exception {
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = ds.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target));
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId.toString())
|
||||
.andExpect(jsonPath("$", hasSize(3)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename0')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename1')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename2')]", hasSize(1)));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("org.eclipse.hawkbit.ddi.rest.resource.DdiInstalledBaseTest#actionTypeForDeployment")
|
||||
@Description("Test forced deployment to a controller. Checks that action is represented as installedBase after installation.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 5), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1) })
|
||||
public void deploymentActionInInstalledBase(final Action.ActionType actionType) throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), target.getControllerId(), actionType));
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Closed")),
|
||||
status().isOk());
|
||||
|
||||
// Run test
|
||||
final ResultActions resultActions = performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId());
|
||||
resultActions.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
containsString(String.format("/%s/controller/v1/%s/installedBase/%d",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), actionId))));
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact, artifactSignature,
|
||||
actionId, ds.findFirstModuleByType(osType).get().getId(), actionType);
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaTypes.HAL_JSON, ds, artifact, artifactSignature, actionId,
|
||||
ds.findFirstModuleByType(osType).get().getId(), actionType);
|
||||
|
||||
// Action is still finished after calling installedBase
|
||||
final Iterable<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Sort.Direction.DESC, "id"), actionId);
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Action.Status.FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download-only deployment to a controller. Checks that download-only is not represented as installedBase.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 2) })
|
||||
public void deploymentDownloadOnlyActionNotInInstalledBase() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), target.getControllerId(), Action.ActionType.DOWNLOAD_ONLY));
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonDownloadDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonDownloadedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
// Test
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("org.eclipse.hawkbit.ddi.rest.resource.DdiInstalledBaseTest#actionTypeForDeployment")
|
||||
@Description("Test a failed deployment to a controller. Checks that closed action is not represented as installedBase.")
|
||||
public void deploymentActionFailedNotInInstalledBase(final Action.ActionType actionType) throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), target.getControllerId(), actionType));
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE),
|
||||
status().isOk());
|
||||
postDeploymentFeedback(target.getControllerId(), actionId,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.FAILURE,
|
||||
Collections.singletonList("Installation failed")),
|
||||
status().isOk());
|
||||
|
||||
// Test
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerInstalledAction endpoint.")
|
||||
public void testActionHistoryCount() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.SCHEDULED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("Installation scheduled")),
|
||||
status().isOk());
|
||||
|
||||
postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("Installation proceeding")),
|
||||
status().isOk());
|
||||
// only this feedback triggers the ActionUpdateEvent
|
||||
postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Installation completed")),
|
||||
status().isOk());
|
||||
|
||||
// Test
|
||||
// for zero input no action history is returned
|
||||
mvc.perform(get(INSTALLED_BASE + "?actionHistory", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages").doesNotExist());
|
||||
|
||||
// depending on given query parameter value, only the latest messages are
|
||||
// returned
|
||||
mvc.perform(get(INSTALLED_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation completed"))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation proceeding"))))
|
||||
.andExpect(
|
||||
jsonPath("$.actionHistory.messages", not(hasItem(containsString("Installation scheduled")))));
|
||||
|
||||
// for negative input the entire action history is returned
|
||||
mvc.perform(get(INSTALLED_BASE + "?actionHistory=-3", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation completed"))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation proceeding"))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation scheduled"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test various invalid access attempts to the installed resource und the expected behaviour of the server.")
|
||||
public void badInstalledAction() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(CONTROLLER_ID);
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), "not-existing", "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// no deployment
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// wrong media type
|
||||
final List<Target> toAssign = Collections.singletonList(target);
|
||||
final DistributionSet savedSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(savedSet, toAssign));
|
||||
postDeploymentFeedback(CONTROLLER_ID, actionId, getJsonClosedCancelActionFeedback(), status().isOk());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, actionId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, actionId)
|
||||
.accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotAcceptable());
|
||||
}
|
||||
|
||||
private static Stream<Action.ActionType> actionTypeForDeployment() {
|
||||
return Stream.of(Action.ActionType.SOFT, Action.ActionType.FORCED);
|
||||
}
|
||||
|
||||
private Target createTargetAndAssertNoActiveActions() {
|
||||
final Target savedTarget = testdataFactory.createTarget(CONTROLLER_ID);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
assertThat(deploymentManagement.countActionsAll()).isZero();
|
||||
assertThat(actionStatusRepository.count()).isZero();
|
||||
return savedTarget;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,731 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.TENANT_CONFIGURATION;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Step;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
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;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTypeCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleTypeCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
|
||||
import org.eclipse.hawkbit.repository.test.util.WithUser;
|
||||
import org.eclipse.hawkbit.rest.util.JsonBuilder;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||
import org.eclipse.hawkbit.util.IpUtil;
|
||||
import org.junit.jupiter.api.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;
|
||||
|
||||
/**
|
||||
* Test the root controller resources.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Root Poll Resource")
|
||||
class DdiRootControllerTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String TARGET_COMPLETED_INSTALLATION_MSG = "Target completed installation.";
|
||||
private static final String TARGET_PROCEEDING_INSTALLATION_MSG = "Target proceeding installation.";
|
||||
private static final String TARGET_SCHEDULED_INSTALLATION_MSG = "Target scheduled installation.";
|
||||
@Autowired
|
||||
private HawkbitSecurityProperties securityProperties;
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the root poll resource is available as CBOR")
|
||||
void rootPollResourceCbor() throws Exception {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711).accept(DdiRestConstants.MEDIA_TYPE_CBOR))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the API returns JSON when no Accept header is specified by the client.")
|
||||
void apiReturnsJSONByDefault() throws Exception {
|
||||
final MvcResult result = mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON)).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,
|
||||
SYSTEM_ROLE }, autoCreateTenant = false)
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetTypeCreatedEvent.class, count = 3),
|
||||
@Expect(type = SoftwareModuleTypeCreatedEvent.class, count = 2) })
|
||||
void targetCannotBeRegisteredIfTenantDoesNotExistsButWhenExists() throws Exception {
|
||||
|
||||
mvc.perform(get("/default-tenant/", tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
// create tenant -- creates softwaremoduletypes and distributionsettypes
|
||||
systemManagement.createTenantMetadata("tenantDoesNotExists");
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), "aControllerId"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
// delete tenant again, will also deleted target aControllerId
|
||||
systemManagement.deleteTenant("tenantDoesNotExists");
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), "aControllerId"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that target poll request does not change audit data on the entity.")
|
||||
@WithUser(principal = "knownPrincipal", authorities = { SpPermission.READ_TARGET, SpPermission.UPDATE_TARGET,
|
||||
SpPermission.CREATE_TARGET }, allSpPermissions = false)
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) })
|
||||
void targetPollDoesNotModifyAuditData() throws Exception {
|
||||
// create target first with "knownPrincipal" user and audit data
|
||||
final String knownTargetControllerId = "target1";
|
||||
final String knownCreatedBy = "knownPrincipal";
|
||||
testdataFactory.createTarget(knownTargetControllerId);
|
||||
final Target findTargetByControllerID = targetManagement.getByControllerID(knownTargetControllerId).get();
|
||||
assertThat(findTargetByControllerID.getCreatedBy()).isEqualTo(knownCreatedBy);
|
||||
|
||||
// make a poll, audit information should not be changed, run as
|
||||
// controller principal!
|
||||
SecurityContextSwitch.runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS),
|
||||
() -> {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownTargetControllerId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
return null;
|
||||
});
|
||||
|
||||
// verify that audit information has not changed
|
||||
final Target targetVerify = targetManagement.getByControllerID(knownTargetControllerId).get();
|
||||
assertThat(targetVerify.getCreatedBy()).isEqualTo(findTargetByControllerID.getCreatedBy());
|
||||
assertThat(targetVerify.getCreatedAt()).isEqualTo(findTargetByControllerID.getCreatedAt());
|
||||
assertThat(targetVerify.getLastModifiedBy()).isEqualTo(findTargetByControllerID.getLastModifiedBy());
|
||||
assertThat(targetVerify.getLastModifiedAt()).isEqualTo(findTargetByControllerID.getLastModifiedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that server returns a not found response in case of empty controller ID.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) })
|
||||
void rootRsWithoutId() throws Exception {
|
||||
mvc.perform(get("/controller/v1/")).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the system creates a new target in plug and play manner, i.e. target is authenticated but does not exist yet.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1) })
|
||||
void rootRsPlugAndPlay() throws Exception {
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
final String controllerId = "4711";
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")));
|
||||
assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
|
||||
assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus())
|
||||
.isEqualTo(TargetUpdateStatus.REGISTERED);
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that tenant specific polling time, which is saved in the db, is delivered to the controller.")
|
||||
@WithUser(principal = "knownpricipal", allSpPermissions = false)
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void pollWithModifiedGlobalPollingTime() throws Exception {
|
||||
SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION),
|
||||
() -> {
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME_INTERVAL,
|
||||
"00:02:00");
|
||||
return null;
|
||||
});
|
||||
|
||||
SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("controller", CONTROLLER_ROLE_ANONYMOUS), () -> {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:02:00")));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that etag check results in not modified response if provided etag by client is identical to entity in repository.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 6),
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 2),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 6),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 2), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), // implicit lock
|
||||
@Expect(type = ActionCreatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void rootRsNotModified() throws Exception {
|
||||
final String controllerId = "4711";
|
||||
final String etag = mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))).andReturn().getResponse()
|
||||
.getHeader("ETag");
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).header("If-None-Match", etag))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified());
|
||||
|
||||
final Target target = targetManagement.getByControllerID(controllerId).get();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), controllerId);
|
||||
|
||||
final Action updateAction = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
final String etagWithFirstUpdate = mvc
|
||||
.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)
|
||||
.header("If-None-Match", etag).accept(MediaType.APPLICATION_JSON).with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink("4711", updateAction.getId().toString()))))
|
||||
.andReturn().getResponse().getHeader("ETag");
|
||||
|
||||
assertThat(etagWithFirstUpdate).isNotNull();
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).header("If-None-Match",
|
||||
etagWithFirstUpdate).with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified());
|
||||
|
||||
// now lets finish the update
|
||||
sendDeploymentActionFeedback(target, updateAction, "closed", null).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// as the update was installed, and we always receive the installed action, the
|
||||
// original state cannot be restored
|
||||
final String etagAfterInstallation = mvc
|
||||
.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)
|
||||
.header("If-None-Match", etag).accept(MediaType.APPLICATION_JSON)
|
||||
.with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink("4711", updateAction.getId().toString()))))
|
||||
.andReturn().getResponse().getHeader("ETag");
|
||||
|
||||
// Now another deployment
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
|
||||
|
||||
assignDistributionSet(ds2.getId(), controllerId);
|
||||
|
||||
final Action updateAction2 = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)
|
||||
.header("If-None-Match", etagAfterInstallation).accept(MediaType.APPLICATION_JSON)
|
||||
.with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink("4711", updateAction.getId().toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink("4711", updateAction2.getId().toString()))))
|
||||
.andReturn().getResponse().getHeader("ETag");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the target state machine of a precomissioned target switches from "
|
||||
+ "UNKNOWN to REGISTERED when the target polls for the first time.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) })
|
||||
void rootRsPrecommissioned() throws Exception {
|
||||
final String controllerId = "4711";
|
||||
testdataFactory.createTarget(controllerId);
|
||||
|
||||
assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus())
|
||||
.isEqualTo(TargetUpdateStatus.UNKNOWN);
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")));
|
||||
|
||||
assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
|
||||
assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus())
|
||||
.isEqualTo(TargetUpdateStatus.REGISTERED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the source IP address of the polling target is correctly stored in repository")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1) })
|
||||
void rootRsPlugAndPlayIpAddress() throws Exception {
|
||||
// test
|
||||
final String knownControllerId1 = "0815";
|
||||
final long create = System.currentTimeMillis();
|
||||
|
||||
// make a poll, audit information should be set on plug and play
|
||||
SecurityContextSwitch.runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS),
|
||||
() -> {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownControllerId1))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
return null;
|
||||
});
|
||||
|
||||
// verify
|
||||
final Target target = targetManagement.getByControllerID(knownControllerId1).get();
|
||||
assertThat(target.getAddress()).isEqualTo(IpUtil.createHttpUri("127.0.0.1"));
|
||||
assertThat(target.getCreatedBy()).isEqualTo("CONTROLLER_PLUG_AND_PLAY");
|
||||
assertThat(target.getCreatedAt()).isGreaterThanOrEqualTo(create);
|
||||
assertThat(target.getLastModifiedBy()).isEqualTo("CONTROLLER_PLUG_AND_PLAY");
|
||||
assertThat(target.getLastModifiedAt()).isGreaterThanOrEqualTo(create);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the source IP address of the polling target is not stored in repository if disabled")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1) })
|
||||
void rootRsIpAddressNotStoredIfDisabled() throws Exception {
|
||||
securityProperties.getClients().setTrackRemoteIp(false);
|
||||
|
||||
// test
|
||||
final String knownControllerId1 = "0815";
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownControllerId1))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
// verify
|
||||
final Target target = targetManagement.getByControllerID(knownControllerId1).get();
|
||||
assertThat(target.getAddress()).isEqualTo(IpUtil.createHttpUri("***"));
|
||||
|
||||
securityProperties.getClients().setTrackRemoteIp(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller trys to finish an update process after it has been finished by an error action status.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2) })
|
||||
void tryToFinishAnUpdateProcessAfterItHasBeenFinished() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "failure").andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success").andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isGone());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller sends attribute update request after device successfully closed software update.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 2),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 6), @Expect(type = TargetPollEvent.class, count = 4),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void attributeUpdateRequestSendingAfterSuccessfulDeployment() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("1");
|
||||
final Target savedTarget = testdataFactory.createTarget("922");
|
||||
final Map<String, String> attributes = Collections.singletonMap("AttributeKey", "AttributeValue");
|
||||
assertThatAttributesUpdateIsRequested(savedTarget.getControllerId());
|
||||
|
||||
mvc.perform(put(CONTROLLER_BASE + "/configData", tenantAware.getCurrentTenant(), savedTarget.getControllerId())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
assertThatAttributesUpdateIsNotRequested(savedTarget.getControllerId());
|
||||
|
||||
assertAttributesUpdateNotRequestedAfterFailedDeployment(savedTarget, ds);
|
||||
|
||||
assertAttributesUpdateRequestedAfterSuccessfulDeployment(savedTarget, ds);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void testActionHistoryCount() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
not(hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG)))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that a zero input value of actionHistory results in no action history appended for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void testActionHistoryZeroInput() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=0", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages").doesNotExist());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that entire action history is returned if the input value for actionHistory is -1, for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void testActionHistoryNegativeInput() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG)
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=-1", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test the polling time based on different maintenance window start and end time.")
|
||||
void sleepTimeResponseForDifferentMaintenanceWindowParameters() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION),
|
||||
() -> {
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME_INTERVAL,
|
||||
"00:05:00");
|
||||
tenantConfigurationManagement
|
||||
.addOrUpdateConfiguration(TenantConfigurationKey.MIN_POLLING_TIME_INTERVAL, "00:01:00");
|
||||
return null;
|
||||
});
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget("1911");
|
||||
assignDistributionSetWithMaintenanceWindow(ds.getId(), savedTarget.getControllerId(), getTestSchedule(16),
|
||||
getTestDuration(10), getTestTimeZone()).getAssignedEntity().iterator().next();
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.config.polling.sleep", greaterThanOrEqualTo("00:05:00")));
|
||||
|
||||
final Target savedTarget1 = testdataFactory.createTarget("2911");
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1");
|
||||
assignDistributionSetWithMaintenanceWindow(ds1.getId(), savedTarget1.getControllerId(), getTestSchedule(10),
|
||||
getTestDuration(10), getTestTimeZone()).getAssignedEntity().iterator().next();
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "2911")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.config.polling.sleep", lessThan("00:05:00")))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", greaterThanOrEqualTo("00:03:00")));
|
||||
|
||||
final Target savedTarget2 = testdataFactory.createTarget("3911");
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
|
||||
assignDistributionSetWithMaintenanceWindow(ds2.getId(), savedTarget2.getControllerId(), getTestSchedule(5),
|
||||
getTestDuration(5), getTestTimeZone()).getAssignedEntity().iterator().next();
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "3911")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.config.polling.sleep", lessThan("00:02:00")));
|
||||
|
||||
final Target savedTarget3 = testdataFactory.createTarget("4911");
|
||||
final DistributionSet ds3 = testdataFactory.createDistributionSet("3");
|
||||
assignDistributionSetWithMaintenanceWindow(ds3.getId(), savedTarget3.getControllerId(), getTestSchedule(-5),
|
||||
getTestDuration(15), getTestTimeZone()).getAssignedEntity().iterator().next();
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "4911")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:05:00")));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download and update values before maintenance window start time.")
|
||||
void downloadAndUpdateStatusBeforeMaintenanceWindowStartTime() throws Exception {
|
||||
Target savedTarget = testdataFactory.createTarget("1911");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow(ds.getId(),
|
||||
savedTarget.getControllerId(), getTestSchedule(2), getTestDuration(1), getTestTimeZone()));
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk());
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.deployment.download", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.update", equalTo("skip")))
|
||||
.andExpect(jsonPath("$.deployment.maintenanceWindow", equalTo("unavailable")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download and update values after maintenance window start time.")
|
||||
void downloadAndUpdateStatusDuringMaintenanceWindow() throws Exception {
|
||||
Target savedTarget = testdataFactory.createTarget("1911");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow(ds.getId(),
|
||||
savedTarget.getControllerId(), getTestSchedule(-5), getTestDuration(10), getTestTimeZone()));
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk());
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.deployment.download", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.update", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.maintenanceWindow", equalTo("available")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Assign multiple DS in multi-assignment mode. The earliest active Action is exposed to the controller.")
|
||||
void earliestActionIsExposedToControllerInMultiAssignMode() throws Exception {
|
||||
enableMultiAssignments();
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet(UUID.randomUUID().toString());
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString());
|
||||
final Action action1 = getFirstAssignedAction(assignDistributionSet(ds1.getId(), target.getControllerId(), 56));
|
||||
final Long action2Id = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), 34));
|
||||
|
||||
assertDeploymentActionIsExposedToTarget(target.getControllerId(), action1.getId());
|
||||
sendDeploymentActionFeedback(target, action1, "closed", "success");
|
||||
assertDeploymentActionIsExposedToTarget(target.getControllerId(), action2Id);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("The system should not create a new target because of a too long controller id.")
|
||||
void rootRsWithInvalidControllerId() throws Exception {
|
||||
final String invalidControllerId = RandomStringUtils.randomAlphabetic(Target.CONTROLLER_ID_MAX_SIZE + 1);
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), invalidControllerId))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void assertAttributesUpdateNotRequestedAfterFailedDeployment(Target target, final DistributionSet ds)
|
||||
throws Exception {
|
||||
target = getFirstAssignedTarget(assignDistributionSet(ds.getId(), target.getControllerId()));
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
sendDeploymentActionFeedback(target, action, "closed", "failure").andExpect(status().isOk());
|
||||
assertThatAttributesUpdateIsNotRequested(target.getControllerId());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void assertAttributesUpdateRequestedAfterSuccessfulDeployment(Target target, final DistributionSet ds)
|
||||
throws Exception {
|
||||
target = getFirstAssignedTarget(assignDistributionSet(ds.getId(), target.getControllerId()));
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
sendDeploymentActionFeedback(target, action, "closed", null).andExpect(status().isOk());
|
||||
assertThatAttributesUpdateIsRequested(target.getControllerId());
|
||||
}
|
||||
|
||||
private void assertThatAttributesUpdateIsRequested(final String targetControllerId) throws Exception {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId)
|
||||
.accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.configData.href").isNotEmpty());
|
||||
}
|
||||
|
||||
private void assertThatAttributesUpdateIsNotRequested(final String targetControllerId) throws Exception {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId)
|
||||
.accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.configData").doesNotExist());
|
||||
}
|
||||
|
||||
private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution,
|
||||
String finished, String message) throws Exception {
|
||||
if (finished == null) {
|
||||
finished = "none";
|
||||
}
|
||||
if (message == null) {
|
||||
message = RandomStringUtils.randomAlphanumeric(1000);
|
||||
}
|
||||
|
||||
final String feedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.valueOf(execution.toUpperCase()),
|
||||
DdiResult.FinalResult.valueOf(finished.toUpperCase()), Collections.singletonList(message));
|
||||
return mvc.perform(
|
||||
post(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), target.getControllerId(), action.getId())
|
||||
.content(feedback).contentType(MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution,
|
||||
final String finished) throws Exception {
|
||||
return sendDeploymentActionFeedback(target, action, execution, finished, null);
|
||||
}
|
||||
|
||||
private void assertDeploymentActionIsExposedToTarget(final String controllerId, final long expectedActionId)
|
||||
throws Exception {
|
||||
final String expectedDeploymentBaseLink = String.format("/%s/controller/v1/%s/deploymentBase/%d",
|
||||
tenantAware.getCurrentTenant(), controllerId, expectedActionId);
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink)));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.security.DosFilter;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Test potential DOS attack scenarios and check if the filter prevents them.
|
||||
*/
|
||||
@ActiveProfiles({ "test" })
|
||||
@Feature("Component Tests - REST Security")
|
||||
@Story("Denial of Service protection filter")
|
||||
class DosFilterTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String X_FORWARDED_FOR = HawkbitSecurityProperties.Clients.X_FORWARDED_FOR;
|
||||
|
||||
@Override
|
||||
protected DefaultMockMvcBuilder createMvcWebAppContext(final WebApplicationContext context) {
|
||||
return super.createMvcWebAppContext(context).addFilter(
|
||||
new DosFilter(null, 10, 10,
|
||||
"127\\.0\\.0\\.1|\\[0:0:0:0:0:0:0:1\\]", "(^192\\.168\\.)",
|
||||
"X-Forwarded-For"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that clients that are on the blacklist are forbidden")
|
||||
void blackListedClientIsForbidden() throws Exception {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "192.168.0.4 , 10.0.0.1 ")).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a READ DoS attempt is blocked ")
|
||||
void getFloodingAttackThatIsPrevented() throws Exception {
|
||||
|
||||
MvcResult result = null;
|
||||
|
||||
int requests = 0;
|
||||
do {
|
||||
result = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "10.0.0.1")).andReturn();
|
||||
requests++;
|
||||
|
||||
// we give up after 1.000 requests
|
||||
assertThat(requests).isLessThan(1_000);
|
||||
} while (result.getResponse().getStatus() != HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
|
||||
// the filter shuts down after 100 GET requests
|
||||
assertThat(requests).isGreaterThanOrEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that an assumed READ DoS attempt is not blocked as the client (with IPv4 address) is on a whitelist")
|
||||
void unacceptableGetLoadButOnWhitelistIPv4() throws Exception {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "127.0.0.1")).andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that an assumed READ DoS attempt is not blocked as the client (with IPv6 address) is on a whitelist")
|
||||
void unacceptableGetLoadButOnWhitelistIPv6() throws Exception {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "0:0:0:0:0:0:0:1")).andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a relatively high number of READ requests is allowed if it is below the DoS detection threshold")
|
||||
@SuppressWarnings("squid:S2925")
|
||||
// No idea how to get rid of the Thread.sleep here
|
||||
void acceptableGetLoad() throws Exception {
|
||||
|
||||
for (int x = 0; x < 3; x++) {
|
||||
// sleep for one second
|
||||
Thread.sleep(1100);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "10.0.0.1")).andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a WRITE DoS attempt is blocked ")
|
||||
void putPostFloddingAttackThatisPrevented() throws Exception {
|
||||
final Long actionId = prepareDeploymentBase();
|
||||
final String feedback = getJsonProceedingDeploymentActionFeedback();
|
||||
|
||||
MvcResult result = null;
|
||||
int requests = 0;
|
||||
do {
|
||||
result = mvc.perform(post("/{tenant}/controller/v1/4711/deploymentBase/" + actionId + "/feedback",
|
||||
tenantAware.getCurrentTenant()).header(X_FORWARDED_FOR, "10.0.0.1").content(feedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andReturn();
|
||||
requests++;
|
||||
|
||||
// we give up after 500 requests
|
||||
assertThat(requests).isLessThan(500);
|
||||
} while (result.getResponse().getStatus() != HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
|
||||
// the filter shuts down after 10 POST requests
|
||||
assertThat(requests).isGreaterThanOrEqualTo(10);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a relatively high number of WRITE requests is allowed if it is below the DoS detection threshold")
|
||||
@SuppressWarnings("squid:S2925")
|
||||
// No idea how to get rid of the Thread.sleep here
|
||||
void acceptablePutPostLoad() throws Exception {
|
||||
final Long actionId = prepareDeploymentBase();
|
||||
final String feedback = getJsonProceedingDeploymentActionFeedback();
|
||||
|
||||
for (int x = 0; x < 5; x++) {
|
||||
// sleep for one second
|
||||
Thread.sleep(1100);
|
||||
|
||||
for (int i = 0; i < 9; i++) {
|
||||
mvc.perform(post("/{tenant}/controller/v1/4711/deploymentBase/" + actionId + "/feedback",
|
||||
tenantAware.getCurrentTenant()).header(X_FORWARDED_FOR, "10.0.0.1")
|
||||
.content(feedback).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Long prepareDeploymentBase() {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("test");
|
||||
final Target target = testdataFactory.createTarget("4711");
|
||||
final List<Target> toAssign = Collections.singletonList(target);
|
||||
|
||||
assignDistributionSet(ds, toAssign);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())).hasSize(1);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
return uaction.getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ddi.rest.resource;
|
||||
|
||||
import static org.eclipse.hawkbit.ddi.rest.resource.AbstractDDiApiIntegrationTest.HTTP_PORT;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
|
||||
public class RequestOnHawkbitDefaultPortPostProcessor implements RequestPostProcessor {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
||||
request.setRemotePort(HTTP_PORT);
|
||||
request.setServerPort(HTTP_PORT);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
#
|
||||
# This program and the accompanying materials are made
|
||||
# available under the terms of the Eclipse Public License 2.0
|
||||
# which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
#
|
||||
# SPDX-License-Identifier: EPL-2.0
|
||||
#
|
||||
|
||||
# DDI configuration - START
|
||||
hawkbit.controller.pollingTime=00:01:00
|
||||
hawkbit.controller.pollingOverdueTime=00:01:00
|
||||
hawkbit.controller.minPollingTime=00:00:30
|
||||
hawkbit.controller.maintenanceWindowPollCount=3
|
||||
# DDI configuration - END
|
||||
# Upload configuration - START
|
||||
spring.servlet.multipart.max-file-size=5MB
|
||||
# Upload configuration - END
|
||||
# Quota - START
|
||||
hawkbit.server.security.dos.maxStatusEntriesPerAction=100
|
||||
hawkbit.server.security.dos.maxAttributeEntriesPerTarget=10
|
||||
# Quota - END
|
||||
# Logging START - activate to see request/response details
|
||||
#logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG
|
||||
# Logging END
|
||||
Reference in New Issue
Block a user