Moves DDI artifacts into a dedicated directory/artifact parent (#2002)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-11 15:11:07 +02:00
committed by GitHub
parent 25c09a7d39
commit 05d8d6cc7e
91 changed files with 153 additions and 67 deletions

View File

@@ -0,0 +1,2 @@
[Spring Boot Starter](http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter) for
the [Direct Device Integration API](https://www.eclipse.org/hawkbit/documentation/interfaces/ddi-api.html).

View File

@@ -0,0 +1,76 @@
<!--
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
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-ddi-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-boot-starter-ddi-api</artifactId>
<name>hawkBit :: Spring Boot Starter DDI API</name>
<dependencies>
<!-- Spring - START -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-aspects</artifactId>
</dependency>
<!-- Spring - END -->
<!-- hawkBit - START -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-ddi-resource</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-security-integration</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-http-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
<!-- hawkBit - END -->
</dependencies>
</project>

View File

@@ -0,0 +1,25 @@
/**
* 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.autoconfigure.ddi;
import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* Auto-Configuration for enabling the DDI REST-Resources.
*/
@Configuration
@ConditionalOnClass(DdiApiConfiguration.class)
@Import(DdiApiConfiguration.class)
public class DDiApiAutoConfiguration {
}

View File

@@ -0,0 +1 @@
org.eclipse.hawkbit.autoconfigure.ddi.DDiApiAutoConfiguration

View File

@@ -0,0 +1,18 @@
# Eclipse.IoT hawkBit - Direct Device Integration API - Model and Resources
The Direct Device Integration (DDI) API is used by devices for communicating with the HawkBit Update Server through
HTTP.
# Version 1
The model follows [semantic versioning](http://semver.org) with MAJOR version, i.e. breaking changes will result in a
new MAJOR version.
# Compile
#### Build hawkbit-ddi-api
```
$ cd hawkbit/hawkbit-ddi-api
$ mvn clean install
```

View File

@@ -0,0 +1,65 @@
<!--
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
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-ddi-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-ddi-api</artifactId>
<name>hawkBit :: REST :: DDI API</name>
<dependencies>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-rest-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- TEST -->
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,68 @@
/**
* 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.json.model;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* <p>
* After the HawkBit Target has executed an action, received by a GET(URL)
* request it reports the completion of it to the HawkBit Server with a action
* status message, i.e. with a PUT message to the feedback channel, i.e. PUT
* URL/feedback. This message could be used not only at the end of execution but
* also as status updates during a longer lasting execution period. The format
* of each action answer message is defined below at each action. But it is
* expected, that the contents of the message answers have all a similar
* structure: The content starts with a generic header and additional elements.
* *
* </p>
*
* <p>
* The answer header would look like: { "time": "20140511T121314", "status": {
* "execution": "closed", "result": { "final": "success", "progress": {} }
* "details": [], } }
* </p>
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiActionFeedback {
@Schema(description = "Timestamp of the action", example = "2023-08-03T12:31:41.890992967Z")
private final String time;
@NotNull
@Valid
private final DdiStatus status;
/**
* Constructs an action-feedback
*
* @param time time of feedback
* @param status status to be appended to the action
*/
@JsonCreator
public DdiActionFeedback(
@JsonProperty(value = "time") final String time,
@JsonProperty(value = "status", required = true) final DdiStatus status) {
this.time = time;
this.status = status;
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2017 Siemens AG
*
* 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.json.model;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi;
/**
* Provide action history information to the controller as part of response to
* {@link DdiRootControllerRestApi#getControllerDeploymentBaseAction} and
* {@link DdiRootControllerRestApi#getConfirmationBaseAction}:
* <ol>
* <li>Current action status at the server</li>
* <li>List of messages from action history</li>
* </ol>
* that were sent to server earlier by the controller using {@link DdiActionFeedback}.
*/
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({ "status", "messages" })
public class DdiActionHistory {
@JsonProperty("status")
@Schema(description = "Status of the deployment based on previous feedback by the device", example = "RUNNING")
private final String actionStatus;
@JsonProperty("messages")
@Schema(description = "Messages are previously sent to the feedback channel in LIFO order by the device. Note: The first status message is set by the system and describes the trigger of the deployment")
private final List<String> messages;
/**
* Parameterized constructor for creating {@link DdiActionHistory}.
*
* @param actionStatus is the current action status at the server
* @param messages is a list of messages retrieved from action history.
*/
@JsonCreator
public DdiActionHistory(
@JsonProperty("status") final String actionStatus,
@JsonProperty("messages") List<String> messages) {
this.actionStatus = actionStatus;
this.messages = messages;
}
}

View File

@@ -0,0 +1,48 @@
/**
* 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.json.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiActivateAutoConfirmation {
@JsonProperty(required = false)
@Schema(description = "Individual value (e.g. username) stored as initiator and automatically used as confirmed" +
" user in future actions", example = "exampleUser")
private final String initiator;
@JsonProperty(required = false)
@Schema(description = "Individual value to attach a remark which will be persisted when automatically " +
"confirming future actions", example = "exampleRemark")
private final String remark;
/**
* Constructor.
*
* @param initiator can be null
* @param remark can be null
*/
@JsonCreator
public DdiActivateAutoConfirmation(@JsonProperty(value = "initiator") final String initiator,
@JsonProperty(value = "remark") final String remark) {
this.initiator = initiator;
this.remark = remark;
}
}

View File

@@ -0,0 +1,73 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.hateoas.RepresentationModel;
/**
* Download information for all artifacts related to a specific {@link DdiChunk}
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = """
**_links**:
* **download** - HTTPs Download resource for artifacts. The resource supports partial download as specified by RFC7233 (range requests). Keep in mind that the target needs to have the artifact assigned in order to be granted permission to download.
* **md5sum** - HTTPs Download resource for MD5SUM file is an optional auto generated artifact that is especially useful for Linux based devices on order to check artifact consistency after download by using the md5sum command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself.
* **download-http** - HTTP Download resource for artifacts. The resource supports partial download as specified by RFC7233 (range requests). Keep in mind that the target needs to have the artifact assigned in order to be granted permission to download. (note: anonymous download needs to be enabled on the service account for non-TLS access)
* **md5sum-http** - HTTP Download resource for MD5SUM file is an optional auto generated artifact that is especially useful for Linux based devices on order to check artifact consistency after download by using the md5sum command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself. (note: anonymous download needs to be enabled on the service account for non-TLS access)
""", example = """
{
"filename" : "binaryFile",
"hashes" : {
"sha1" : "e4e667b70ff652cb9d9c8a49f141bd68e06cec6f",
"md5" : "13793b0e3a7830ed685d3ede7ff93048",
"sha256" : "c51368bf045803b429a67bdf04539a373d9fb8caa310fe0431265e6871b4f07a"
},
"size" : 11,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile.MD5SUM"
}
}
}""")
public class DdiArtifact extends RepresentationModel<DdiArtifact> {
@NotNull
@JsonProperty
@Schema(description = "File name", example = "binary.tgz")
private String filename;
@JsonProperty
@Schema(description = "Artifact hashes")
private DdiArtifactHash hashes;
@JsonProperty
@Schema(description = "Artifact size", example = "3")
private Long size;
}

View File

@@ -0,0 +1,57 @@
/**
* 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.json.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* Hashes for given Artifact
*/
@NoArgsConstructor // needed for json create
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiArtifactHash {
@JsonProperty
@Schema(description = "SHA1 hash of the artifact in Base 16 format", example = "2d86c2a659e364e9abba49ea6ffcd53dd5559f05")
private String sha1;
@JsonProperty
@Schema(description = "MD5 hash of the artifact", example = "0d1b08c34858921bc7c662b228acb7ba")
private String md5;
@JsonProperty
@JsonInclude(Include.NON_NULL)
@Schema(description = "SHA-256 hash of the artifact in Base 16 format", example = "a03b221c6c6eae7122ca51695d456d5222e524889136394944b2f9763b483615")
private String sha256;
/**
* Public constructor.
*
* @param sha1 sha1 hash of the artifact
* @param md5 md5 hash of the artifact
* @param sha256 sha256 hash of the artifact
*/
public DdiArtifactHash(final String sha1, final String md5, final String sha256) {
this.sha1 = sha1;
this.md5 = md5;
this.sha256 = sha256;
}
}

View File

@@ -0,0 +1,54 @@
/**
* 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.json.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Allow a target to declare running distribution set version
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiAssignedVersion {
@Schema(description = "Distribution Set name", example = "linux")
private final String name;
@Schema(description = "Distribution set version", example = "1.2.3")
private final String version;
/**
* Constructor
*
* @param name Distribution set name
* @param version Distribution set version
*/
@JsonCreator
public DdiAssignedVersion(@JsonProperty(value = "name", required = true) String name,
@JsonProperty(value = "version", required = true) String version) {
this.name = name;
this.version = version;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
@Override
public String toString() {
return "DdiInstalledVersion{" + "name='" + name + '\'' + ", version='" + version + '\'' + '}';
}
}

View File

@@ -0,0 +1,54 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.hateoas.RepresentationModel;
@NoArgsConstructor // needed for json create
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({ "active", "initiator", "remark", "activatedAt" })
public class DdiAutoConfirmationState extends RepresentationModel<DdiAutoConfirmationState> {
@NotNull
@Schema(example = "true")
private boolean active;
@Schema(example = "exampleUserId")
private String initiator;
@Schema(example = "exampleRemark")
private String remark;
@Schema(example = "1691065895439")
private Long activatedAt;
public static DdiAutoConfirmationState active(final long activatedAt) {
final DdiAutoConfirmationState state = new DdiAutoConfirmationState();
state.setActive(true);
state.setActivatedAt(activatedAt);
return state;
}
public static DdiAutoConfirmationState disabled() {
return new DdiAutoConfirmationState();
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* Cancel action to be provided to the target.
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiCancel {
@Schema(description = "Id of the action", example = "11")
private final String id;
@Schema(description = "Action that needs to be canceled")
@NotNull
private final DdiCancelActionToStop cancelAction;
/**
* Parameterized constructor.
*
* @param id of the cancel action
* @param cancelAction the action
*/
@JsonCreator
public DdiCancel(@JsonProperty("id") final String id,
@JsonProperty("cancelAction") final DdiCancelActionToStop cancelAction) {
this.id = id;
this.cancelAction = cancelAction;
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* The action that has to be stopped by the target.
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiCancelActionToStop {
@NotNull
@Schema(description = "Id of the action that needs to be canceled (typically identical to id field on the cancel action itself)", example = "11")
private final String stopId;
/**
* Parameterized constructor.
*
* @param stopId ID of the action to be stopped
*/
@JsonCreator
public DdiCancelActionToStop(@JsonProperty("stopId") final String stopId) {
this.stopId = stopId;
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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.json.model;
import java.util.Collections;
import java.util.List;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* Deployment chunks.
*/
@NoArgsConstructor // needed for json create
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiChunk {
@JsonProperty("part")
@NotNull
@Schema(description = "Type of the chunk, e.g. firmware, bundle, app. In update server mapped to Software Module Type")
private String part;
@JsonProperty("version")
@NotNull
@Schema(description = "Software version of the chunk", example = "1.2.0")
private String version;
@JsonProperty("name")
@NotNull
@Schema(description = "Name of the chunk")
private String name;
@JsonProperty("encrypted")
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "If encrypted")
private Boolean encrypted;
@JsonProperty("artifacts")
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "List of artifacts")
private List<DdiArtifact> artifacts;
@JsonProperty("metadata")
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "Meta data of the respective software module that has been marked with 'target visible'")
private List<DdiMetadata> metadata;
/**
* Constructor.
*
* @param part of the deployment chunk
* @param version of the artifact
* @param name of the artifact
* @param encrypted if artifacts are encrypted
* @param artifacts download information
* @param metadata optional as additional information for the target/device
*/
public DdiChunk(final String part, final String version, final String name, final Boolean encrypted,
final List<DdiArtifact> artifacts, final List<DdiMetadata> metadata) {
this.part = part;
this.version = version;
this.name = name;
this.encrypted = encrypted;
this.artifacts = artifacts;
this.metadata = metadata;
}
public List<DdiArtifact> getArtifacts() {
if (artifacts == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(artifacts);
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.json.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* Standard configuration for the target.
*/
@NoArgsConstructor // needed for json deserialization
@Getter
@EqualsAndHashCode
@ToString
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "DDI controller configuration")
public class DdiConfig {
@JsonProperty
private DdiPolling polling;
/**
* Constructor.
*
* @param polling configuration of the polling for the target
*/
public DdiConfig(final DdiPolling polling) {
this.polling = polling;
}
}

View File

@@ -0,0 +1,61 @@
/**
* 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.json.model;
import java.util.Map;
import jakarta.validation.constraints.NotEmpty;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* Feedback channel for ConfigData action.
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(example = """
{
"mode" : "merge",
"data" : {
"VIN" : "JH4TB2H26CC000000",
"hwRevision" : "2"
}
}""")
public class DdiConfigData {
@NotEmpty
@Schema(description = "Link which is provided whenever the provisioning target or device is supposed to push its configuration data (aka. \"controller attributes\") to the server. Only shown for the initial configuration, after a successful update action, or if requested explicitly (e.g. via the management UI).")
private final Map<String, String> data;
@Schema(description = "Optional parameter to specify the update mode that should be applied when updating target attributes. Valid values are 'merge', 'replace', and 'remove'. Defaults to 'merge'.")
private final DdiUpdateMode mode;
/**
* Constructor.
*
* @param data contains the attributes.
* @param mode defines the mode of the update (replace, merge, remove)
*/
@JsonCreator
public DdiConfigData(
@JsonProperty(value = "data") final Map<String, String> data,
@JsonProperty(value = "mode") final DdiUpdateMode mode) {
this.data = data;
this.mode = mode;
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.hateoas.RepresentationModel;
/**
* Confirmation base response.
* Set order to place links at last.
*/
@NoArgsConstructor // needed for json create
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({ "autoConfirm" })
@Schema(
description = """
**_links**:
* **confirmationBase** - confirmation base
* **deactivateAutoConfirm** - where to deactivate auto confirm
* **activateAutoConfirm** - where to activate auto confirm
""",
example = """
{
"autoConfirm" : {
"active" : false
},
"_links" : {
"activateAutoConfirm" : {
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/confirmationBase/activateAutoConfirm"
},
"confirmationBase" : {
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/confirmationBase/10?c=-2122565939"
}
}
}""")
public class DdiConfirmationBase extends RepresentationModel<DdiConfirmationBase> {
@JsonProperty("autoConfirm")
@NotNull
private DdiAutoConfirmationState autoConfirm;
/**
* Constructor.
*
* @param autoConfirmState
*/
public DdiConfirmationBase(final DdiAutoConfirmationState autoConfirmState) {
this.autoConfirm = autoConfirmState;
}
}

View File

@@ -0,0 +1,240 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.hateoas.RepresentationModel;
/**
* Update action resource.
*/
@NoArgsConstructor // needed for json create
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({ "id", "confirmation", "actionHistory" })
@Schema(description = """
The response body includes the detailed information about the action awaiting confirmation in the same format as
for the deploymentBase operation.""",
example = """
{
"id" : "6",
"confirmation" : {
"download" : "forced",
"update" : "forced",
"maintenanceWindow" : "available",
"chunks" : [ {
"part" : "jvm",
"version" : "1.0.62",
"name" : "oneapp runtime",
"artifacts" : [ {
"filename" : "binary.tgz",
"hashes" : {
"sha1" : "3dceccec02e7626184bdbba12b247b67ff04c363",
"md5" : "a9a7df0aa4c72b3b03b654c42d29744b",
"sha256" : "971d8db88fef8e7a3e6d5bbf501d69b07d0c300d9be948aff8b52960ef039358"
},
"size" : 11,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz.MD5SUM"
}
}
}, {
"filename" : "file.signature",
"hashes" : {
"sha1" : "3dceccec02e7626184bdbba12b247b67ff04c363",
"md5" : "a9a7df0aa4c72b3b03b654c42d29744b",
"sha256" : "971d8db88fef8e7a3e6d5bbf501d69b07d0c300d9be948aff8b52960ef039358"
},
"size" : 11,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature.MD5SUM"
}
}
} ]
}, {
"part" : "bApp",
"version" : "1.0.96",
"name" : "oneapplication",
"artifacts" : [ {
"filename" : "binary.tgz",
"hashes" : {
"sha1" : "701c0c0fcbee5e96fa5c5b819cb519686940ade3",
"md5" : "f0f6a34c4c9e79d07c2d92c3c3d88560",
"sha256" : "cff472a07c3143741fb03ac6c577acabef72a186a8bfaab00bbb47ca5ebbe554"
},
"size" : 11,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz.MD5SUM"
}
}
}, {
"filename" : "file.signature",
"hashes" : {
"sha1" : "701c0c0fcbee5e96fa5c5b819cb519686940ade3",
"md5" : "f0f6a34c4c9e79d07c2d92c3c3d88560",
"sha256" : "cff472a07c3143741fb03ac6c577acabef72a186a8bfaab00bbb47ca5ebbe554"
},
"size" : 11,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature.MD5SUM"
}
}
} ]
}, {
"part" : "os",
"version" : "1.0.44",
"name" : "one Firmware",
"artifacts" : [ {
"filename" : "binary.tgz",
"hashes" : {
"sha1" : "2b09765e953cd138b7da8f4725e48183dab62aec",
"md5" : "9b0aa2f51379cb4a5e0b7d026c2605c9",
"sha256" : "618faa741070b3f8148bad06f088e537a8f7913e734df4dde61fb163725cb4ee"
},
"size" : 15,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz.MD5SUM"
}
}
}, {
"filename" : "file.signature",
"hashes" : {
"sha1" : "2b09765e953cd138b7da8f4725e48183dab62aec",
"md5" : "9b0aa2f51379cb4a5e0b7d026c2605c9",
"sha256" : "618faa741070b3f8148bad06f088e537a8f7913e734df4dde61fb163725cb4ee"
},
"size" : 15,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature.MD5SUM"
}
}
} ],
"metadata" : [ {
"key" : "aMetadataKey",
"value" : "Metadata value as defined in software module"
} ]
} ]
},
"actionHistory" : {
"status" : "WAIT_FOR_CONFIRMATION",
"messages" : [ "Assignment initiated by user 'TestPrincipal'", "Waiting for the confirmation by the device before processing with the deployment" ]
}
}""")
public class DdiConfirmationBaseAction extends RepresentationModel<DdiConfirmationBaseAction> {
@JsonProperty("id")
@NotNull
@Schema(description = "Id of the action", example = "6")
private String id;
@JsonProperty("confirmation")
@NotNull
@Schema(description = "Deployment confirmation operation")
private DdiDeployment confirmation;
/**
* Action history containing current action status and a list of feedback
* messages received earlier from the controller.
*/
@JsonProperty("actionHistory")
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = """
(Optional) GET parameter to retrieve a given number of messages which are previously
provided by the device. Useful if the devices sent state information to the feedback channel and never
stored them locally""")
private DdiActionHistory actionHistory;
/**
* Constructor.
*
* @param id of the update action
* @param confirmation chunk details
* @param actionHistory containing current action status and a list of feedback messages
* received earlier from the controller.
*/
public DdiConfirmationBaseAction(final String id, final DdiDeployment confirmation,
final DdiActionHistory actionHistory) {
this.id = id;
this.confirmation = confirmation;
this.actionHistory = actionHistory;
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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.json.model;
import java.util.Collections;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* New update actions require confirmation when confirmation flow is switched on. This is the feedback channel for
* confirmation messages for DDI API. The confirmation message has a mandatory field confirmation with possible values:
* "confirmed" and "denied".
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiConfirmationFeedback {
@NotNull
@Valid
@Schema(description = "Action confirmation state")
private final Confirmation confirmation;
@Schema(description = "(Optional) Individual status code", example = "200")
private final Integer code;
@Schema(description = "List of detailed message information", example = "[ \"Feedback message\" ]")
private final List<String> details;
/**
* Constructs an confirmation-feedback
*
* @param confirmation confirmation value for the action. Valid values are "Confirmed" and "Denied
* @param code code for confirmation
* @param details messages
*/
@JsonCreator
public DdiConfirmationFeedback(
@JsonProperty(value = "confirmation", required = true) final Confirmation confirmation,
@JsonProperty(value = "code") final Integer code,
@JsonProperty(value = "details") final List<String> details) {
this.confirmation = confirmation;
this.code = code;
this.details = details;
}
public List<String> getDetails() {
if (details == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(details);
}
public enum Confirmation {
/**
* Confirm the action.
*/
CONFIRMED("confirmed"),
/**
* Deny the action.
*/
DENIED("denied");
private final String name;
Confirmation(final String name) {
this.name = name;
}
@JsonValue
public String getName() {
return name;
}
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.json.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.hateoas.RepresentationModel;
/**
* {@link DdiControllerBase} resource content.
*/
@NoArgsConstructor // needed for json deserialization
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = """
**_links**: Actions that the server has for the target
* **deploymentBase** - Detailed deployment operation
* **installedBase** - Detailed operation of last successfully finished action
* **configData** - Link which is provided whenever the provisioning target or device is supposed to push its configuration data (aka. "controller attributes") to the server. Only shown for the initial configuration, after a successful update action, or if requested explicitly (e.g. via the management UI)
""",
example = """
{
"config" : {
"polling" : {
"sleep" : "12:00:00"
}
},
"_links" : {
"deploymentBase" : {
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/deploymentBase/5?c=-2127183556"
},
"installedBase" : {
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/installedBase/4"
},
"configData" : {
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/configData"
}
}
}""")
public class DdiControllerBase extends RepresentationModel<DdiControllerBase> {
@JsonProperty
private DdiConfig config;
/**
* Constructor.
*
* @param config configuration of the SP target
*/
public DdiControllerBase(final DdiConfig config) {
this.config = config;
}
}

View File

@@ -0,0 +1,147 @@
/**
* 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.json.model;
import java.util.Collections;
import java.util.List;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* Detailed update action information.
*/
@NoArgsConstructor // needed for json create
@Getter
@EqualsAndHashCode
@ToString
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiDeployment {
@Schema(description = """
Handling for the download part of the provisioning process ('skip': do not download yet, 'attempt': server asks
to download, 'forced': server requests immediate download)""")
private HandlingType download;
@Schema(description = """
Handling for the update part of the provisioning process ('skip': do not update yet,
'attempt': server asks to update, 'forced': server requests immediate update)""")
private HandlingType update;
@JsonProperty("chunks")
@NotNull
@Schema(description = "Software chunks of an update. In server mapped by Software Module")
private List<DdiChunk> chunks;
@Schema(description = """
Separation of download and installation by defining a maintenance window for the installation. Status shows if
currently in a window""")
private DdiMaintenanceWindowStatus maintenanceWindow;
/**
* Constructor.
*
* @param download handling type
* @param update handling type
* @param chunks to handle.
* @param maintenanceWindow specifying whether there is a maintenance schedule associated.
* If it is, the value is either 'available' (i.e. the
* maintenance window is now available as per defined schedule
* and the update can progress) or 'unavailable' (implying that
* maintenance window is not available now and update should not
* be attempted). If there is no maintenance schedule defined,
* the parameter is null.
*/
public DdiDeployment(final HandlingType download, final HandlingType update, final List<DdiChunk> chunks,
final DdiMaintenanceWindowStatus maintenanceWindow) {
this.download = download;
this.update = update;
this.chunks = chunks;
this.maintenanceWindow = maintenanceWindow;
}
public List<DdiChunk> getChunks() {
if (chunks == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(chunks);
}
/**
* The handling type for the update action.
*/
public enum HandlingType {
/**
* Not necessary for the command.
*/
SKIP("skip"),
/**
* Try to execute (local applications may intervene by SP control API).
*/
ATTEMPT("attempt"),
/**
* Execution independent of local intervention attempts.
*/
FORCED("forced");
private String name;
HandlingType(final String name) {
this.name = name;
}
@JsonValue
public String getName() {
return name;
}
}
/**
* Status of the maintenance window for action.
*/
public enum DdiMaintenanceWindowStatus {
/**
* A window is currently available, target can go ahead with
* installation.
*/
AVAILABLE("available"),
/**
* A window is not available, target should wait and skip the
* installation.
*/
UNAVAILABLE("unavailable");
private String status;
DdiMaintenanceWindowStatus(final String status) {
this.status = status;
}
/**
* @return status of maintenance window.
*/
@JsonValue
public String getStatus() {
return this.status;
}
}
}

View File

@@ -0,0 +1,233 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.hateoas.RepresentationModel;
/**
* Update action resource.
*/
@NoArgsConstructor // needed for json create
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({ "id", "deployment", "actionHistory" })
@Schema(example = """
{
"id" : "8",
"deployment" : {
"download" : "forced",
"update" : "forced",
"maintenanceWindow" : "available",
"chunks" : [ {
"part" : "jvm",
"version" : "1.0.75",
"name" : "oneapp runtime",
"artifacts" : [ {
"filename" : "binary.tgz",
"hashes" : {
"sha1" : "986a1ade8b8a2f758ce951340cc5e21335cc2a00",
"md5" : "d04440e6533863247655ac5fd4345bcc",
"sha256" : "b3a04740a19e36057ccf258701922f3cd2f1a880536be53a3ca8d50f6b615975"
},
"size" : 13,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz.MD5SUM"
}
}
}, {
"filename" : "file.signature",
"hashes" : {
"sha1" : "986a1ade8b8a2f758ce951340cc5e21335cc2a00",
"md5" : "d04440e6533863247655ac5fd4345bcc",
"sha256" : "b3a04740a19e36057ccf258701922f3cd2f1a880536be53a3ca8d50f6b615975"
},
"size" : 13,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature.MD5SUM"
}
}
} ]
}, {
"part" : "os",
"version" : "1.0.79",
"name" : "one Firmware",
"artifacts" : [ {
"filename" : "binary.tgz",
"hashes" : {
"sha1" : "574cd34be20f75d101ed23518339cc38c5157bdb",
"md5" : "a0637c1ccb9fd53e2ba6f45712516989",
"sha256" : "498014801aab66be1d7fbea56b1aa5959651b6fd710308e196a8c414029e7291"
},
"size" : 13,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz.MD5SUM"
}
}
}, {
"filename" : "file.signature",
"hashes" : {
"sha1" : "574cd34be20f75d101ed23518339cc38c5157bdb",
"md5" : "a0637c1ccb9fd53e2ba6f45712516989",
"sha256" : "498014801aab66be1d7fbea56b1aa5959651b6fd710308e196a8c414029e7291"
},
"size" : 13,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature.MD5SUM"
}
}
} ]
}, {
"part" : "bApp",
"version" : "1.0.91",
"name" : "oneapplication",
"artifacts" : [ {
"filename" : "binary.tgz",
"hashes" : {
"sha1" : "e3ba7ff5839c210c98e254dde655147ffc49f5c9",
"md5" : "020017c498e6b0b8f76168fd55fa6fd1",
"sha256" : "80406288820379a82bbcbfbf7e8690146e46256f505de1c6d430c0168a74f6dd"
},
"size" : 11,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz.MD5SUM"
}
}
}, {
"filename" : "file.signature",
"hashes" : {
"sha1" : "e3ba7ff5839c210c98e254dde655147ffc49f5c9",
"md5" : "020017c498e6b0b8f76168fd55fa6fd1",
"sha256" : "80406288820379a82bbcbfbf7e8690146e46256f505de1c6d430c0168a74f6dd"
},
"size" : 11,
"_links" : {
"download" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
},
"download-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
},
"md5sum-http" : {
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature.MD5SUM"
},
"md5sum" : {
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature.MD5SUM"
}
}
} ],
"metadata" : [ {
"key" : "aMetadataKey",
"value" : "Metadata value as defined in software module"
} ]
} ]
},
"actionHistory" : {
"status" : "RUNNING",
"messages" : [ "Reboot", "Write firmware", "Download done", "Download failed. ErrorCode #5876745. Retry", "Started download", "Assignment initiated by user 'TestPrincipal'" ]
}
}""")
public class DdiDeploymentBase extends RepresentationModel<DdiDeploymentBase> {
@JsonProperty("id")
@NotNull
@Schema(description = "Id of the action", example = "8")
private String id;
@JsonProperty("deployment")
@NotNull
@Schema(description = "Detailed deployment operation")
private DdiDeployment deployment;
/**
* Action history containing current action status and a list of feedback
* messages received earlier from the controller.
*/
@JsonProperty("actionHistory")
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "Current deployment state")
private DdiActionHistory actionHistory;
/**
* Constructor.
*
* @param id of the update action
* @param deployment details
* @param actionHistory containing current action status and a list of feedback
* messages received earlier from the controller.
*/
public DdiDeploymentBase(final String id, final DdiDeployment deployment, final DdiActionHistory actionHistory) {
this.id = id;
this.deployment = deployment;
this.actionHistory = actionHistory;
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* Additional metadata to be provided for the target/device.
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiMetadata {
@JsonProperty
@NotNull
@Schema(description = "Key of meta data entry")
private final String key;
@JsonProperty
@NotNull
@Schema(description = "Value of meta data entry")
private final String value;
@JsonCreator
public DdiMetadata(@JsonProperty("key") final String key, @JsonProperty("value") final String value) {
this.key = key;
this.value = value;
}
}

View File

@@ -0,0 +1,49 @@
/**
* 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.json.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Polling interval for the SP target.
*/
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "Suggested sleep time between polls")
public class DdiPolling {
@JsonProperty
@Schema(description = "Sleep time in HH:MM:SS notation", pattern = "HH:MM:SS", example = "12:00:00")
private String sleep;
/**
* Constructor.
*
* @param sleep between polls
*/
public DdiPolling(final String sleep) {
this.sleep = sleep;
}
/**
* Constructor.
*/
public DdiPolling() {
// needed for json create
}
public String getSleep() {
return sleep;
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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.json.model;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* Action fulfillment progress by means of gives the achieved amount of maximal
* of possible levels.
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiProgress {
@NotNull
@Schema(description = "Achieved amount", example = "2")
private final Integer cnt;
@Schema(description = "Maximum levels", example = "5")
private final Integer of;
/**
* Constructor.
*
* @param cnt achieved amount
* @param of maximum levels
*/
@JsonCreator
public DdiProgress(@JsonProperty("cnt") final Integer cnt, @JsonProperty("of") final Integer of) {
this.cnt = cnt;
this.of = of;
}
}

View File

@@ -0,0 +1,84 @@
/**
* 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.json.model;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* Result information of the action progress which can by an intermediate or
* final update.
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiResult {
@NotNull
@Valid
@Schema(description = "Result of the action execution")
private final FinalResult finished;
@Schema(description = "Progress assumption of the device (currently not supported)")
private final DdiProgress progress;
/**
* Constructor.
*
* @param finished as final result
* @param progress if not yet finished
*/
@JsonCreator
public DdiResult(@JsonProperty("finished") final FinalResult finished,
@JsonProperty("progress") final DdiProgress progress) {
this.finished = finished;
this.progress = progress;
}
/**
* Defined status of the final result.
*/
public enum FinalResult {
/**
* Execution was successful.
*/
SUCCESS("success"),
/**
* Execution terminated with errors or without the expected result.
*/
FAILURE("failure"),
/**
* No final result could be determined (yet).
*/
NONE("none");
private String name;
FinalResult(final String name) {
this.name = name;
}
@JsonValue
public String getName() {
return name;
}
}
}

View File

@@ -0,0 +1,132 @@
/**
* 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.json.model;
import java.util.Collections;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
/**
* Details status information concerning the action processing.
*/
@Getter
@EqualsAndHashCode
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "Target action status")
public class DdiStatus {
@NotNull
@Valid
@Schema(description = "Status of the action execution")
private final ExecutionStatus execution;
@NotNull
@Valid
@Schema(description = "Result of the action execution")
private final DdiResult result;
@Schema(description = "(Optional) Individual status code", example = "200")
private final Integer code;
@Schema(description = "List of details message information", example = "[ \"Some feedback\" ]")
private final List<String> details;
/**
* Constructor.
*
* @param execution status
* @param result information
* @param code as optional code (can be null)
* @param details as optional addition
*/
@JsonCreator
public DdiStatus(@JsonProperty("execution") final ExecutionStatus execution,
@JsonProperty("result") final DdiResult result, @JsonProperty("code") final Integer code,
@JsonProperty("details") final List<String> details) {
this.execution = execution;
this.result = result;
this.code = code;
this.details = details;
}
public List<String> getDetails() {
if (details == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(details);
}
/**
* The element status contains information about the execution of the
* operation.
*/
public enum ExecutionStatus {
/**
* Execution of the action has finished.
*/
CLOSED("closed"),
/**
* Execution has started but has not yet finished.
*/
PROCEEDING("proceeding"),
/**
* Execution was suspended from outside.
*/
CANCELED("canceled"),
/**
* Action has been noticed and is intended to run.
*/
SCHEDULED("scheduled"),
/**
* Action was not accepted.
*/
REJECTED("rejected"),
/**
* Action is started after a reset, power loss, etc.
*/
RESUMED("resumed"),
/**
* The action has been downloaded by the target.
*/
DOWNLOADED("downloaded"),
/**
* Target starts to download.
*/
DOWNLOAD("download");
private String name;
ExecutionStatus(final String name) {
this.name = name;
}
@JsonValue
public String getName() {
return name;
}
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2018 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.json.model;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Enumerates the supported update modes. Each mode represents an attribute
* update strategy.
*
* @see DdiConfigData
*/
public enum DdiUpdateMode {
/**
* Merge update strategy
*/
MERGE("merge"),
/**
* Replacement update strategy
*/
REPLACE("replace"),
/**
* Removal update strategy
*/
REMOVE("remove");
private String name;
DdiUpdateMode(final String name) {
this.name = name;
}
@JsonValue
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,84 @@
/**
* 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.api;
/**
* Constants for the direct device integration rest resources.
*/
public final class DdiRestConstants {
/**
* The base URL mapping of the direct device integration rest resources.
*/
public static final String BASE_V1_REQUEST_MAPPING = "/{tenant}/controller/v1";
/**
* Deployment action resources.
*/
public static final String DEPLOYMENT_BASE_ACTION = "deploymentBase";
/**
* Confirmation base resource.
*/
public static final String CONFIRMATION_BASE = "confirmationBase";
/**
* Activate auto-confirm
*/
public static final String AUTO_CONFIRM_ACTIVATE = "activateAutoConfirm";
/**
* Deactivate auto-confirm
*/
public static final String AUTO_CONFIRM_DEACTIVATE = "deactivateAutoConfirm";
/**
* Installed action resources.
*/
public static final String INSTALLED_BASE_ACTION = "installedBase";
/**
* Cancel action resources.
*/
public static final String CANCEL_ACTION = "cancelAction";
/**
* Feedback channel.
*/
public static final String FEEDBACK = "feedback";
/**
* File suffix for MDH hash download (see Linux md5sum).
*/
public static final String ARTIFACT_MD5_DWNL_SUFFIX = ".MD5SUM";
/**
* Config data action resources.
*/
public static final String CONFIG_DATA_ACTION = "configData";
/**
* Default value specifying that no action history to be sent as part of
* response to deploymentBase
* {@link DdiRootControllerRestApi#getControllerDeploymentBaseAction}.
* {@link DdiRootControllerRestApi#getConfirmationBaseAction}.
*/
public static final String NO_ACTION_HISTORY = "0";
/**
* Media type for CBOR content. Unfortunately, there is no other constant we
* can reuse - even the Jackson data converter simply hardcodes this.
*/
public static final String MEDIA_TYPE_CBOR = "application/cbor";
private DdiRestConstants() {
// constant class, private constructor.
}
}

View File

@@ -0,0 +1,806 @@
/**
* 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.api;
import java.io.InputStream;
import java.lang.annotation.Target;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
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.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.DdiDeploymentBase;
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
* REST resource handling for root controller CRUD operations.
*/
// no request mapping specified here to avoid CVE-2021-22044 in Feign client
@Tag(name = "DDI Root Controller", description = "REST resource handling for root controller CRUD operations")
public interface DdiRootControllerRestApi {
/**
* Returns all artifacts of a given software module and target.
*
* @param tenant of the client
* @param controllerId of the target that matches to controller id
* @param softwareModuleId of the software module
* @return the response
*/
@Operation(summary = "Return all artifacts of a given software module and target",
description = "Returns all artifacts that are assigned to the software module")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
+ "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts", produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<List<DdiArtifact>> getSoftwareModulesArtifacts(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") final String controllerId,
@PathVariable("softwareModuleId") final Long softwareModuleId);
/**
* Root resource for an individual {@link Target}.
*
* @param tenant of the request
* @param controllerId of the target that matches to controller id
* @return the response
*/
@Operation(summary = "Root resource for an individual Target", description = """
This base resource can be regularly polled by the controller on the provisioning target or device in order to
retrieve actions that need to be executed. Those are provided as a list of links to give more detailed
information about the action. Links are only available for initial configuration, open actions, or the latest
installed action, respectively. The resource supports Etag based modification checks in order to save traffic.
Note: deployments have to be confirmed in order to move on to the next action. Cancellations have to be
confirmed or rejected.""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to " +
"be changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}", produces = {
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<DdiControllerBase> getControllerBase(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") final String controllerId);
/**
* Handles GET {@link DdiArtifact} download request. This could be full or
* partial (as specified by RFC7233 (Range Requests)) download request.
*
* @param tenant of the request
* @param controllerId of the target
* @param softwareModuleId of the parent software module
* @param fileName of the related local artifact
* @return response of the servlet which in case of success is status code
* {@link HttpStatus#OK} or in case of partial download
* {@link HttpStatus#PARTIAL_CONTENT}.
*/
@Operation(summary = "Artifact download", description = "Handles GET DdiArtifact download request. This could be " +
"full or partial (as specified by RFC7233 (Range Requests)) download request.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be" +
" changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target or Module not found",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts" +
" and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
+ "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{fileName}")
ResponseEntity<InputStream> downloadArtifact(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") final String controllerId,
@PathVariable("softwareModuleId") final Long softwareModuleId,
@PathVariable("fileName") final String fileName);
/**
* Handles GET {@link DdiArtifact} MD5 checksum file download request.
*
* @param tenant of the request
* @param controllerId of the target
* @param softwareModuleId of the parent software module
* @param fileName of the related local artifact
* @return {@link ResponseEntity} with status {@link HttpStatus#OK} if
* successful
*/
@Operation(summary = "MD5 checksum download",
description = "Handles GET {@link DdiArtifact} MD5 checksum file download request.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target or Module not found",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
+ "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{fileName}"
+ DdiRestConstants.ARTIFACT_MD5_DWNL_SUFFIX, produces = MediaType.TEXT_PLAIN_VALUE)
ResponseEntity<Void> downloadArtifactMd5(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") final String controllerId,
@PathVariable("softwareModuleId") final Long softwareModuleId,
@PathVariable("fileName") final String fileName);
/**
* Resource for software module.
*
* @param tenant of the request
* @param controllerId of the target
* @param actionId of the {@link DdiDeploymentBase} that matches to active
* actions.
* @param resource an hashcode of the resource which indicates if the action has
* been changed, e.g. from 'soft' to 'force' and the eTag needs
* to be re-generated
* @param actionHistoryMessageCount specifies the number of messages to be returned from action
* history. Regardless of the passed value, in order to restrict
* resource utilization by controllers, maximum number of
* messages that are retrieved from database is limited by
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
*
* actionHistoryMessageCount less than zero: retrieves the
* maximum allowed number of action status messages from history;
*
* actionHistoryMessageCount equal to zero: does not retrieve any
* message;
*
* actionHistoryMessageCount greater than zero: retrieves the
* specified number of messages, limited by maximum allowed
* number.
* @return the response
*/
@Operation(summary = "Resource for software module (Deployment Base)", description = """
Core resource for deployment operations. Contains all information necessary in order to execute the operation.
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
Host, port and path and not guaranteed to be similar to the provided examples below but will be defined at
runtime.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = """
Successfully retrieved
In case a device provides state information on the feedback channel and wont store it locally,
a query for, e.q, the last 10 messages, could be used which will include the previously provided by the
device,
feedback.
In addition to the straight forward approach to inform the device to download and install the software
in one transaction hawkBit supports the separation of download and installation into separate steps.
This feature is called Maintenance Window where the device is informed to download the software first
and then when it enters a defined (maintenance) window the installation triggers follows as usual.
"""),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403",
description = "Insufficient permissions, entity is not allowed to be changed (i.e. read-only) or " +
"data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<DdiDeploymentBase> getControllerDeploymentBaseAction(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@PathVariable("actionId") @NotNull final Long actionId,
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "c", required = false, defaultValue = "-1") final int resource,
@RequestParam(
value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "actionHistory",
defaultValue = DdiRestConstants.NO_ACTION_HISTORY)
@Schema(description = """
(Optional) GET parameter to retrieve a given number of messages which are previously provided by the
device. Useful if the devices sent state information to the feedback channel and never stored them
locally.""") final Integer actionHistoryMessageCount);
/**
* This is the feedback channel for the {@link DdiDeploymentBase} action.
*
* @param tenant of the client
* @param feedback to provide
* @param controllerId of the target that matches to controller id
* @param actionId of the action we have feedback for
* @return the response
*/
@Operation(summary = "Feedback channel for the DeploymentBase action", description = """
Feedback channel. It is up to the device how much intermediate feedback is provided.
However, the action will be kept open until the controller on the device reports a finished (either successful
or error).
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target not found",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
"user in another request at the same time. You may retry your modification request.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "410", description = "Action is not active anymore.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
"supported by the server for this resource.",
content = @Content(mediaType = "application/json",
schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json",
schema = @Schema(hidden = true)))
})
@PostMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/" + DdiRestConstants.FEEDBACK, consumes = {
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<Void> postDeploymentBaseActionFeedback(@Valid final DdiActionFeedback feedback,
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
@PathVariable("actionId") @NotNull final Long actionId);
/**
* This is the feedback channel for the config data action.
*
* @param tenant of the client
* @param configData as body
* @param controllerId to provide data for
* @return status of the request
*/
@Operation(summary = "Feedback channel for the config data action", description = """
The usual behaviour is that when a new device registers at the server it is requested to provide the meta
information that will allow the server to identify the device on a hardware level (e.g. hardware revision,
mac address, serial number etc.).""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
"user in another request at the same time. You may retry your modification request.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
"supported by the server for this resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@PutMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.CONFIG_DATA_ACTION, consumes = { MediaType.APPLICATION_JSON_VALUE,
DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<Void> putConfigData(@Valid final DdiConfigData configData,
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId);
/**
* RequestMethod.GET method for the {@link DdiCancel} action.
*
* @param tenant of the request
* @param controllerId ID of the calling target
* @param actionId of the action
* @return the {@link DdiCancel} response
*/
@Operation(summary = "Cancel an action", description = """
The Hawkbit server might cancel an operation, e.g. an unfinished update has a successor. It is up to the
provisioning target to decide to accept the cancelation or reject it.""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION
+ "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE,
DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<DdiCancel> getControllerCancelAction(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@PathVariable("actionId") @NotNull final Long actionId);
/**
* RequestMethod.POST method receiving the {@link DdiActionFeedback} from
* the target.
*
* @param feedback the {@link DdiActionFeedback} from the target.
* @param tenant of the client
* @param controllerId the ID of the calling target
* @param actionId of the action we have feedback for
* @return the {@link DdiActionFeedback} response
*/
@Operation(summary = "Feedback channel for cancel actions", description = """
It is up to the device how much intermediate feedback is provided. However, the action will be kept open
until the controller on the device reports a finished (either successful or error) or rejects the action,
e.g. the canceled actions have been started already.""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
"user in another request at the same time. You may retry your modification request.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
"supported by the server for this resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@PostMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION
+ "/{actionId}/" + DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE,
DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<Void> postCancelActionFeedback(@Valid final DdiActionFeedback feedback,
@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@PathVariable("actionId") @NotNull final Long actionId);
/**
* Resource for installed distribution set to retrieve the last successfully
* finished action.
*
* @param tenant of the request
* @param controllerId of the target
* @param actionId of the {@link DdiDeploymentBase} that matches to installed
* action.
* @param actionHistoryMessageCount specifies the number of messages to be returned from action
* history. Regardless of the passed value, in order to restrict
* resource utilization by controllers, maximum number of
* messages that are retrieved from database is limited by
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
*
* actionHistoryMessageCount less than zero: retrieves the
* maximum allowed number of action status messages from history;
*
* actionHistoryMessageCount equal to zero: does not retrieve any
* message;
*
* actionHistoryMessageCount greater than zero: retrieves the
* specified number of messages, limited by maximum allowed
* number.
* @return the {@link DdiDeploymentBase}. The response is of same format as
* for the /deploymentBase resource.
*/
@Operation(summary = "Previously installed action", description = """
Resource to receive information of the previous installation. Can be used to re-retrieve artifacts of
the already finished action, for example in case a re-installation is necessary. The response will be of
the same format as the deploymentBase operation, providing the previous action that has been finished
successfully. As the action is already finished, no further feedback is expected.
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
Host, port and path are not guaranteed to be similar to the provided examples below but will be defined at
runtime.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = """
The response body includes the detailed operation for the already finished action in the same format as
for the deploymentBase operation.
In this case the (optional) query for the last 10 messages, previously provided by the device, are included.
"""),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.INSTALLED_BASE_ACTION + "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<DdiDeploymentBase> getControllerInstalledAction(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@PathVariable("actionId") @NotNull final Long actionId,
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
+ "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount);
/**
* Returns the confirmation base with the current auto-confirmation state
* for a given controllerId and toggle links. In case there are actions
* present where the confirmation is required, a reference to it will be
* returned as well.
*
* @param tenant the controllerId is corresponding too
* @param controllerId to check the state for
* @return the state as {@link DdiAutoConfirmationState}
*/
@Operation(summary = "Resource to request confirmation specific information for the controller", description = """
Core resource for confirmation related operations. While active actions awaiting confirmation will be
referenced, the current auto-confirmation status will be shown. In case auto-confirmation is active, details
like the initiator, remark and date of activation (as unix timestamp) will be provided.
Reference links to switch the auto-confirmation state are exposed as well.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = """
The response body in case auto-confirmation is active is richer - it contains additional information
such as initiator, remark and when the auto-confirmation had been activated.
"""),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.CONFIRMATION_BASE, produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<DdiConfirmationBase> getConfirmationBase(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId);
/**
* Resource for confirmation of an action.
*
* @param tenant of the request
* @param controllerId of the target
* @param actionId of the {@link DdiConfirmationBaseAction} that matches to
* active actions in WAITING_FOR_CONFIRMATION status.
* @param resource an hashcode of the resource which indicates if the action has
* been changed, e.g. from 'soft' to 'force' and the eTag needs
* to be re-generated
* @param actionHistoryMessageCount specifies the number of messages to be returned from action
* history. Regardless of the passed value, in order to restrict
* resource utilization by controllers, maximum number of
* messages that are retrieved from database is limited by
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
*
* actionHistoryMessageCount less than zero: retrieves the
* maximum allowed number of action status messages from history;
*
* actionHistoryMessageCount equal to zero: does not retrieve any
* message;
*
* actionHistoryMessageCount greater than zero: retrieves the
* specified number of messages, limited by maximum allowed
* number.
* @return the response
*/
@Operation(summary = "Confirmation status of an action", description = """
Resource to receive information about a pending confirmation. The response will be of the same format as the
deploymentBase operation. The controller should provide feedback about the confirmation first, before
processing the deployment.
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
Host, port and path are not guaranteed to be similar to the provided examples below but will be defined at
runtime.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "The response body includes the detailed information about " +
"the action awaiting confirmation in the same format as for the deploymentBase operation."),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target not found",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.CONFIRMATION_BASE + "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<DdiConfirmationBaseAction> getConfirmationBaseAction(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@PathVariable("actionId") @NotNull final Long actionId,
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
+ "c", required = false, defaultValue = "-1") final int resource,
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
+ "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount);
/**
* This is the feedback channel for the {@link DdiConfirmationBaseAction}
* action.
*
* @param tenant of the client
* @param feedback to provide
* @param controllerId of the target that matches to controller id
* @param actionId of the action we have feedback for
* @return the response
*/
@Operation(summary = "Feedback channel for actions waiting for confirmation", description = """
The device will use this resource to either confirm or deny an action which is waiting for confirmation. The
action will be transferred into the RUNNING state in case the device is confirming it. Afterwards it will be
exposed by the deploymentBase.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target or Action not found",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
"user in another request at the same time. You may retry your modification request.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "410", description = "Action is not active anymore.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
"supported by the server for this resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@PostMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.CONFIRMATION_BASE + "/{actionId}/" + DdiRestConstants.FEEDBACK, consumes = {
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<Void> postConfirmationActionFeedback(@Valid final DdiConfirmationFeedback feedback,
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
@PathVariable("actionId") @NotNull final Long actionId);
/**
* Activate auto confirmation for a given controllerId. Will use the
* provided initiator and remark field from the provided
* {@link DdiActivateAutoConfirmation}. If not present, the values will be
* prefilled with a default remark and the CONTROLLER as initiator.
*
* @param tenant the controllerId is corresponding too
* @param controllerId to activate auto-confirmation for
* @param body as {@link DdiActivateAutoConfirmation}
* @return {@link org.springframework.http.HttpStatus#OK} if successful or
* {@link org.springframework.http.HttpStatus#CONFLICT} in case
* auto-confirmation was active already.
*/
@Operation(summary = "Interface to activate auto-confirmation for a specific device", description = """
The device can use this resource to activate auto-confirmation. As a result all current active as well as
future actions will automatically be confirmed by mentioning the initiator as triggered person. Actions will
be automatically confirmed, as long as auto-confirmation is active.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target not found",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
"user in another request at the same time. You may retry your modification request.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
"supported by the server for this resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@PostMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.CONFIRMATION_BASE + "/" + DdiRestConstants.AUTO_CONFIRM_ACTIVATE, consumes = {
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<Void> activateAutoConfirmation(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@Valid @RequestBody(required = false) final DdiActivateAutoConfirmation body);
/**
* Deactivate auto confirmation for a given controller id.
*
* @param tenant the controllerId is corresponding too
* @param controllerId to disable auto-confirmation for
* @return {@link org.springframework.http.HttpStatus#OK} if successfully executed
*/
@Operation(summary = "Interface to deactivate auto-confirmation for a specific controller", description = """
The device can use this resource to deactivate auto-confirmation. All active actions will remain unchanged
while all future actions need to be confirmed, before processing with the deployment.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
"changed (i.e. read-only) or data volume restriction applies.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target not found",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
"user in another request at the same time. You may retry your modification request.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
"supported by the server for this resource.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
"and the client has to wait another second.",
content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@PostMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.CONFIRMATION_BASE + "/" + DdiRestConstants.AUTO_CONFIRM_DEACTIVATE)
ResponseEntity<Void> deactivateAutoConfirmation(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId);
/**
* Assign an already installed distribution for a target
*
* @param tenant of the client
* to provide
* @param controllerId of the target that matches to controller id
* @param ddiAssignedVersion as {@link DdiAssignedVersion}
* @return the response
*/
@Operation(summary = "Set offline assigned version", description = """
Allow to set current running version.
This method is EXPERIMENTAL and may change in future releases.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
@ApiResponse(responseCode = "401", description = "The request requires user authentication.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be changed (i.e. read-only) or data volume restriction applies.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", description = "Target or Distribution not found", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another user in another request at the same time. You may retry your modification request.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "410", description = "Action is not active anymore.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not supported by the server for this resource.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts and the client has to wait another second.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
})
@PutMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.INSTALLED_BASE_ACTION, consumes = {
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity<Void> setAsssignedOfflineVersion(@Valid DdiAssignedVersion ddiAssignedVersion,
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId);
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import java.time.Instant;
import java.util.Collections;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
/**
* Test serialization of DDI api model 'DdiActionFeedback'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serialization of DDI api Models")
class DdiActionFeedbackTest {
private final ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model with minimal payload")
void shouldSerializeAndDeserializeObjectWithoutOptionalValues() throws IOException {
// Setup
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, null, null, Lists.emptyList());
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(null, ddiStatus);
// Test
final String serializedDdiActionFeedback = mapper.writeValueAsString(ddiActionFeedback);
final DdiActionFeedback deserializedDdiActionFeedback = mapper.readValue(serializedDdiActionFeedback,
DdiActionFeedback.class);
assertThat(deserializedDdiActionFeedback.getStatus()).hasToString(ddiStatus.toString());
}
@Test
@Description("Verify the correct serialization and deserialization of the model with all values provided")
void shouldSerializeAndDeserializeObjectWithOptionalValues() throws IOException {
// Setup
final String time = Instant.now().toString();
final DdiResult ddiResult = new DdiResult(DdiResult.FinalResult.SUCCESS, new DdiProgress(10, 10));
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, ddiResult, 200, Collections.singletonList("myMessage"));
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(time, ddiStatus);
// Test
final String serializedDdiActionFeedback = mapper.writeValueAsString(ddiActionFeedback);
final DdiActionFeedback deserializedDdiActionFeedback = mapper.readValue(serializedDdiActionFeedback,
DdiActionFeedback.class);
assertThat(serializedDdiActionFeedback).contains(time);
assertThat(deserializedDdiActionFeedback.getTime()).isEqualTo(time);
assertThat(deserializedDdiActionFeedback.getStatus()).hasToString(ddiStatus.toString());
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
final String serializedDdiActionFeedback = "{\"time\":\"20190809T121314\",\"status\":{\"execution\": [closed],\"result\":null,\"details\":[]}}";
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiActionFeedback, DdiActionFeedback.class));
}
@Test
@Description("Verify that deserialization works if optional fields are not parsed")
void shouldConvertItWithoutOptionalFieldTime() throws JsonProcessingException {
// Setup
final String serializedDdiActionFeedback = "{\n" + //
" \"status\" : {\n" + //
" \"result\" : {\n" + //
" \"finished\" : \"none\"\n" + //
" },\n" + //
" \"execution\" : \"download\",\n" + //
" \"details\" : [ \"Some message\" ]\n" + //
" }\n" + //
"}";//
assertThat(mapper.readValue(serializedDdiActionFeedback, DdiActionFeedback.class)).satisfies(deserializedDdiActionFeedback -> {
assertThat(deserializedDdiActionFeedback.getTime()).isNull();
assertThat(deserializedDdiActionFeedback.getStatus()).isNotNull();
assertThat(deserializedDdiActionFeedback.getStatus().getResult()).isNotNull();
assertThat(deserializedDdiActionFeedback.getStatus().getResult().getFinished()).isEqualTo(DdiResult.FinalResult.NONE);
assertThat(deserializedDdiActionFeedback.getStatus().getResult().getProgress()).isNull();
assertThat(deserializedDdiActionFeedback.getStatus().getCode()).isNull();
assertThat(deserializedDdiActionFeedback.getStatus().getExecution()).isEqualTo(DdiStatus.ExecutionStatus.DOWNLOAD);
assertThat(deserializedDdiActionFeedback.getStatus().getDetails()).hasSize(1);
});
}
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiActionHistory'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiActionHistoryTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
final String actionStatus = "TestAction";
final List<String> messages = Arrays.asList("Action status message 1", "Action status message 2");
final DdiActionHistory ddiActionHistory = new DdiActionHistory(actionStatus, messages);
// Test
final String serializedDdiActionHistory = OBJECT_MAPPER.writeValueAsString(ddiActionHistory);
final DdiActionHistory deserializedDdiActionHistory = OBJECT_MAPPER.readValue(serializedDdiActionHistory,
DdiActionHistory.class);
assertThat(serializedDdiActionHistory).contains(actionStatus, messages.get(0), messages.get(1));
assertThat(deserializedDdiActionHistory.toString()).contains(actionStatus, messages.get(0), messages.get(1));
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
final String serializedDdiActionHistory = """
{
"status": "SomeAction",
"messages": [ "Some message"],
"unknownProperty": "test"
}""";
// Test
final DdiActionHistory ddiActionHistory = OBJECT_MAPPER.readValue(serializedDdiActionHistory, DdiActionHistory.class);
assertThat(ddiActionHistory.toString()).contains("SomeAction", "Some message");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
final String serializedDdiActionFeedback = """
{
"status": [SomeAction],
"messages": ["Some message"]
}""";
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> OBJECT_MAPPER.readValue(serializedDdiActionFeedback, DdiActionHistory.class));
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiArtifactHash'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiArtifactHashTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
String sha1Hash = "11111";
String md5Hash = "22222";
String sha256Hash = "33333";
DdiArtifactHash DdiArtifact = new DdiArtifactHash(sha1Hash, md5Hash, sha256Hash);
// Test
String serializedDdiArtifact = mapper.writeValueAsString(DdiArtifact);
DdiArtifactHash deserializedDdiArtifact = mapper.readValue(serializedDdiArtifact,
DdiArtifactHash.class);
assertThat(serializedDdiArtifact).contains(sha1Hash, md5Hash, sha256Hash);
assertThat(deserializedDdiArtifact.getSha1()).isEqualTo(sha1Hash);
assertThat(deserializedDdiArtifact.getMd5()).isEqualTo(md5Hash);
assertThat(deserializedDdiArtifact.getSha256()).isEqualTo(sha256Hash);
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiArtifact = "{\"sha1\": \"123\", \"md5\": \"456\", \"sha256\": \"789\", \"unknownProperty\": \"test\"}";
// Test
DdiArtifactHash ddiArtifact = mapper.readValue(serializedDdiArtifact, DdiArtifactHash.class);
assertThat(ddiArtifact.getSha1()).isEqualTo("123");
assertThat(ddiArtifact.getMd5()).isEqualTo("456");
assertThat(ddiArtifact.getSha256()).isEqualTo("789");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiArtifact = "{\"sha1\": [123], \"md5\": 456, \"sha256\": \"789\"";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiArtifact, DdiArtifactHash.class));
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiArtifact'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiArtifactTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
String filename = "testfile.txt";
DdiArtifactHash hashes = new DdiArtifactHash("123", "456", "789");
Long size = 12345L;
DdiArtifact ddiArtifact = new DdiArtifact();
ddiArtifact.setFilename(filename);
ddiArtifact.setHashes(hashes);
ddiArtifact.setSize(size);
// Test
String serializedDdiArtifact = mapper.writeValueAsString(ddiArtifact);
DdiArtifact deserializedDdiArtifact = mapper.readValue(serializedDdiArtifact, DdiArtifact.class);
assertThat(serializedDdiArtifact).contains(filename, "12345");
assertThat(deserializedDdiArtifact.getFilename()).isEqualTo(filename);
assertThat(deserializedDdiArtifact.getSize()).isEqualTo(size);
assertThat(deserializedDdiArtifact.getHashes().getSha1()).isEqualTo(hashes.getSha1());
assertThat(deserializedDdiArtifact.getHashes().getMd5()).isEqualTo(hashes.getMd5());
assertThat(deserializedDdiArtifact.getHashes().getSha256()).isEqualTo(hashes.getSha256());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiArtifact = "{\"filename\":\"test.file\",\"hashes\":{\"sha1\":\"123\",\"md5\":\"456\",\"sha256\":\"789\"},\"size\":111,\"links\":[],\"unknownProperty\": \"test\"}";
// Test
DdiArtifact ddiArtifact = mapper.readValue(serializedDdiArtifact, DdiArtifact.class);
assertThat(ddiArtifact.getFilename()).isEqualTo("test.file");
assertThat(ddiArtifact.getSize()).isEqualTo(111);
assertThat(ddiArtifact.getHashes().getSha1()).isEqualTo("123");
assertThat(ddiArtifact.getHashes().getMd5()).isEqualTo("456");
assertThat(ddiArtifact.getHashes().getSha256()).isEqualTo("789");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiArtifact = "{\"filename\": [\"test.file\"],\"hashes\":{\"sha1\":\"123\",\"md5\":\"456\",\"sha256\":\"789\"},\"size\":111,\"links\":[]}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiArtifact, DdiArtifact.class));
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiCancelActionToStop'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiCancelActionToStopTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
String stopId = "1234";
DdiCancelActionToStop ddiCancelActionToStop = new DdiCancelActionToStop(stopId);
// Test
String serializedDdiCancelActionToStop = mapper.writeValueAsString(ddiCancelActionToStop);
DdiCancelActionToStop deserializedDdiCancelActionToStop = mapper.readValue(serializedDdiCancelActionToStop,
DdiCancelActionToStop.class);
assertThat(serializedDdiCancelActionToStop).contains(stopId);
assertThat(deserializedDdiCancelActionToStop.getStopId()).isEqualTo(stopId);
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiCancelActionToStop = "{\"stopId\":\"12345\",\"unknownProperty\":\"test\"}";
// Test
DdiCancelActionToStop ddiCancelActionToStop = mapper.readValue(serializedDdiCancelActionToStop,
DdiCancelActionToStop.class);
assertThat(ddiCancelActionToStop.getStopId()).contains("12345");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiCancelActionToStop = "{\"stopId\": [\"12345\"]}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiCancelActionToStop, DdiCancelActionToStop.class));
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiArtifact'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiCancelTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
String ddiCancelId = "1234";
DdiCancelActionToStop ddiCancelActionToStop = new DdiCancelActionToStop("1234");
DdiCancel ddiCancel = new DdiCancel(ddiCancelId, ddiCancelActionToStop);
// Test
String serializedDdiCancel = mapper.writeValueAsString(ddiCancel);
DdiCancel deserializedDdiCancel = mapper.readValue(serializedDdiCancel, DdiCancel.class);
assertThat(serializedDdiCancel).contains(ddiCancelId, ddiCancelActionToStop.getStopId());
assertThat(deserializedDdiCancel.getId()).isEqualTo(ddiCancelId);
assertThat(deserializedDdiCancel.getCancelAction().getStopId()).isEqualTo(ddiCancelActionToStop.getStopId());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiCancel = "{\"id\":\"1234\",\"cancelAction\":{\"stopId\":\"1234\"}, \"unknownProperty\": \"test\"}";
// Test
DdiCancel ddiCancel = mapper.readValue(serializedDdiCancel, DdiCancel.class);
assertThat(ddiCancel.getId()).isEqualTo("1234");
assertThat(ddiCancel.getCancelAction().getStopId()).matches("1234");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiCancel = "{\"id\":[\"1234\"],\"cancelAction\":{\"stopId\":\"1234\"}}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiCancel, DdiCancel.class));
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiChunk'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiChunkTest {
private final ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
final String part = "1234";
final String version = "1.0";
final String name = "Dummy-Artifact";
final List<DdiArtifact> dummyArtifacts = Collections.emptyList();
final DdiChunk ddiChunk = new DdiChunk(part, version, name, null, dummyArtifacts, null);
// Test
final String serializedDdiChunk = mapper.writeValueAsString(ddiChunk);
final DdiChunk deserializedDdiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class);
assertThat(serializedDdiChunk).contains(part, version, name);
assertThat(deserializedDdiChunk.getPart()).isEqualTo(part);
assertThat(deserializedDdiChunk.getVersion()).isEqualTo(version);
assertThat(deserializedDdiChunk.getName()).isEqualTo(name);
assertThat(deserializedDdiChunk.getArtifacts().size()).isEqualTo(0);
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
final String serializedDdiChunk = "{\"part\":\"1234\",\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[],\"unknownProperty\":\"test\"}";
// Test
final DdiChunk ddiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class);
assertThat(ddiChunk.getPart()).isEqualTo("1234");
assertThat(ddiChunk.getVersion()).isEqualTo("1.0");
assertThat(ddiChunk.getName()).isEqualTo("Dummy-Artifact");
assertThat(ddiChunk.getArtifacts().size()).isEqualTo(0);
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
final String serializedDdiChunk = "{\"part\":[\"1234\"],\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[]}";
// Test
assertThatExceptionOfType(MismatchedInputException.class)
.isThrownBy(() -> mapper.readValue(serializedDdiChunk, DdiChunk.class));
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiConfigData'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiConfigDataTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
Map<String, String> data = new HashMap<>();
data.put("test", "data");
DdiConfigData ddiConfigData = new DdiConfigData(data, DdiUpdateMode.REPLACE);
// Test
String serializedDdiConfigData = mapper.writeValueAsString(ddiConfigData);
DdiConfigData deserializedDdiConfigData = mapper.readValue(serializedDdiConfigData, DdiConfigData.class);
assertThat(serializedDdiConfigData).contains("test", "data");
assertThat(deserializedDdiConfigData.getMode()).isEqualTo(DdiUpdateMode.REPLACE);
}
@Test
@Description("Verify the correct deserialization of a model with an additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiConfigData = "{\"data\":{\"test\":\"data\"},\"mode\":\"replace\",\"unknownProperty\":\"test\"}";
// Test
DdiConfigData ddiConfigData = mapper.readValue(serializedDdiConfigData, DdiConfigData.class);
assertThat(ddiConfigData.getMode()).isEqualTo(DdiUpdateMode.REPLACE);
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiConfigData = "{\"data\":{\"test\":\"data\"},\"mode\":[\"replace\"],\"unknownProperty\":\"test\"}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiConfigData, DdiConfigData.class));
}
@Test
@Description("Verify the correct deserialization of a model with removed unused status property")
public void shouldDeserializeObjectWithStatusProperty() throws IOException {
// We formerly falsely required a 'status' property object when using the
// configData endpoint. It was removed as a requirement from code and
// documentation, as it was unused. This test ensures we still behave correctly
// (and just ignore the 'status' property) if it is still provided by the
// client.
// Setup
String serializedDdiConfigData = "{\"id\":123,\"time\":\"20190809T121314\","
+ "\"status\":{\"execution\":\"closed\",\"result\":{\"finished\":\"success\",\"progress\":null},"
+ "\"details\":[]},\"data\":{\"test\":\"data\"},\"mode\":\"replace\"}";
// Test
DdiConfigData ddiConfigData = mapper.readValue(serializedDdiConfigData, DdiConfigData.class);
assertThat(ddiConfigData.getMode()).isEqualTo(DdiUpdateMode.REPLACE);
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiConfig'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiConfigTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
DdiPolling ddiPolling = new DdiPolling("10");
DdiConfig ddiConfig = new DdiConfig(ddiPolling);
// Test
String serializedDdiConfig = mapper.writeValueAsString(ddiConfig);
DdiConfig deserializedDdiConfig = mapper.readValue(serializedDdiConfig, DdiConfig.class);
assertThat(serializedDdiConfig).contains("10");
assertThat(deserializedDdiConfig.getPolling().getSleep()).isEqualTo("10");
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiConfig = "{\"polling\":{\"sleep\":\"123\"},\"unknownProperty\":\"test\"}";
// Test
DdiConfig ddiConfig = mapper.readValue(serializedDdiConfig, DdiConfig.class);
assertThat(ddiConfig.getPolling().getSleep()).isEqualTo("123");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiConfig = "{\"polling\":{\"sleep\":[\"10\"]}}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiConfig, DdiConfig.class));
}
}

View File

@@ -0,0 +1,103 @@
/**
* 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus.AVAILABLE;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.ATTEMPT;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.FORCED;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiConfirmationBase'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("CHeck JSON serialization of DDI api confirmation models")
class DdiConfirmationBaseTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
final String id = "1234";
final DdiDeployment ddiDeployment = new DdiDeployment(FORCED, ATTEMPT, Collections.emptyList(), AVAILABLE);
final String actionStatus = "TestAction";
final DdiActionHistory ddiActionHistory = new DdiActionHistory(actionStatus,
Arrays.asList("Action status message 1", "Action status message 2"));
final DdiConfirmationBaseAction ddiConfirmationBaseAction = new DdiConfirmationBaseAction(id, ddiDeployment,
ddiActionHistory);
// Test
String serializedDdiConfirmationBase = OBJECT_MAPPER.writeValueAsString(ddiConfirmationBaseAction);
final DdiConfirmationBaseAction deserializedDdiConfigurationBase = OBJECT_MAPPER
.readValue(serializedDdiConfirmationBase, DdiConfirmationBaseAction.class);
assertThat(serializedDdiConfirmationBase).contains(id, FORCED.getName(), ATTEMPT.getName(),
AVAILABLE.getStatus(), actionStatus);
assertThat(deserializedDdiConfigurationBase.getConfirmation().getDownload())
.isEqualTo(ddiDeployment.getDownload());
assertThat(deserializedDdiConfigurationBase.getConfirmation().getUpdate()).isEqualTo(ddiDeployment.getUpdate());
assertThat(deserializedDdiConfigurationBase.getConfirmation().getMaintenanceWindow())
.isEqualTo(ddiDeployment.getMaintenanceWindow());
assertThat(deserializedDdiConfigurationBase.getActionHistory().toString())
.isEqualTo(ddiActionHistory.toString());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
final String serializedDdiConfirmationBase = "{" +
"\"id\":\"1234\",\"confirmation\":{\"download\":\"forced\"," +
"\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]}," +
"\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\"," +
"\"Action status message 2\"]},\"links\":[],\"unknownProperty\":\"test\"" +
"}";
// Test
final DdiConfirmationBaseAction ddiConfirmationBaseAction = OBJECT_MAPPER.readValue(serializedDdiConfirmationBase,
DdiConfirmationBaseAction.class);
assertThat(ddiConfirmationBaseAction.getConfirmation().getDownload().getName()).isEqualTo(FORCED.getName());
assertThat(ddiConfirmationBaseAction.getConfirmation().getUpdate().getName()).isEqualTo(ATTEMPT.getName());
assertThat(ddiConfirmationBaseAction.getConfirmation().getMaintenanceWindow().getStatus())
.isEqualTo(AVAILABLE.getStatus());
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
final String serializedDdiConfirmationBase = "{" +
"\"id\":[\"1234\"],\"confirmation\":{\"download\":\"forced\"," +
"\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]}," +
"\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\"," +
"\"Action status message 2\"]},\"links\":[]" +
"}";
// Test
assertThatExceptionOfType(MismatchedInputException.class)
.isThrownBy(() -> OBJECT_MAPPER.readValue(serializedDdiConfirmationBase, DdiConfirmationBaseAction.class));
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiControllerBase'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiControllerBaseTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
// Setup
DdiPolling ddiPolling = new DdiPolling("10");
DdiConfig ddiConfig = new DdiConfig(ddiPolling);
DdiControllerBase ddiControllerBase = new DdiControllerBase(ddiConfig);
// Test
String serializedDdiControllerBase = mapper.writeValueAsString(ddiControllerBase);
DdiControllerBase deserializedDdiControllerBase = mapper.readValue(serializedDdiControllerBase,
DdiControllerBase.class);
assertThat(serializedDdiControllerBase).contains(ddiPolling.getSleep());
assertThat(deserializedDdiControllerBase.getConfig().getPolling().getSleep()).isEqualTo(ddiPolling.getSleep());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiControllerBase = "{\"config\":{\"polling\":{\"sleep\":\"123\"}},\"links\":[],\"unknownProperty\":\"test\"}";
// Test
DdiControllerBase ddiControllerBase = mapper.readValue(serializedDdiControllerBase, DdiControllerBase.class);
assertThat(ddiControllerBase.getConfig().getPolling().getSleep()).isEqualTo("123");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiControllerBase = "{\"config\":{\"polling\":{\"sleep\":[\"123\"]}},\"links\":[],\"unknownProperty\":\"test\"}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiControllerBase, DdiControllerBase.class));
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus.AVAILABLE;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.ATTEMPT;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.FORCED;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiDeploymentBase'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiDeploymentBaseTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
String id = "1234";
DdiDeployment ddiDeployment = new DdiDeployment(FORCED, ATTEMPT, Collections.emptyList(), AVAILABLE);
String actionStatus = "TestAction";
DdiActionHistory ddiActionHistory = new DdiActionHistory(actionStatus,
Arrays.asList("Action status message 1", "Action status message 2"));
DdiDeploymentBase ddiDeploymentBase = new DdiDeploymentBase(id, ddiDeployment, ddiActionHistory);
// Test
String serializedDdiDeploymentBase = mapper.writeValueAsString(ddiDeploymentBase);
DdiDeploymentBase deserializedDdiDeploymentBase = mapper.readValue(serializedDdiDeploymentBase,
DdiDeploymentBase.class);
assertThat(serializedDdiDeploymentBase).contains(id, FORCED.getName(), ATTEMPT.getName(), AVAILABLE.getStatus(),
actionStatus);
assertThat(deserializedDdiDeploymentBase.getDeployment().getDownload()).isEqualTo(ddiDeployment.getDownload());
assertThat(deserializedDdiDeploymentBase.getDeployment().getUpdate()).isEqualTo(ddiDeployment.getUpdate());
assertThat(deserializedDdiDeploymentBase.getDeployment().getMaintenanceWindow()).isEqualTo(
ddiDeployment.getMaintenanceWindow());
assertThat(deserializedDdiDeploymentBase.getActionHistory().toString()).isEqualTo(ddiActionHistory.toString());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiDeploymentBase = "{\"id\":\"1234\",\"deployment\":{\"download\":\"forced\","
+ "\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]},"
+ "\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\","
+ "\"Action status message 2\"]},\"links\":[],\"unknownProperty\":\"test\"}";
// Test
DdiDeploymentBase ddiDeploymentBase = mapper.readValue(serializedDdiDeploymentBase, DdiDeploymentBase.class);
assertThat(ddiDeploymentBase.getDeployment().getDownload().getName()).isEqualTo(FORCED.getName());
assertThat(ddiDeploymentBase.getDeployment().getUpdate().getName()).isEqualTo(ATTEMPT.getName());
assertThat(ddiDeploymentBase.getDeployment().getMaintenanceWindow().getStatus()).isEqualTo(
AVAILABLE.getStatus());
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiDeploymentBase = "{\"id\":[\"1234\"],\"deployment\":{\"download\":\"forced\","
+ "\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]},"
+ "\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\","
+ "\"Action status message 2\"]},\"links\":[]}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiDeploymentBase, DdiDeploymentBase.class));
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus.AVAILABLE;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.ATTEMPT;
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.FORCED;
import java.io.IOException;
import java.util.Collections;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiDeployment'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiDeploymentTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
DdiDeployment ddiDeployment = new DdiDeployment(FORCED, ATTEMPT, Collections.emptyList(), AVAILABLE);
// Test
String serializedDdiDeployment = mapper.writeValueAsString(ddiDeployment);
DdiDeployment deserializedDdiDeployment = mapper.readValue(serializedDdiDeployment, DdiDeployment.class);
assertThat(serializedDdiDeployment).contains(ddiDeployment.getDownload().getName(),
ddiDeployment.getMaintenanceWindow().getStatus());
assertThat(deserializedDdiDeployment.getDownload().getName()).isEqualTo(ddiDeployment.getDownload().getName());
assertThat(deserializedDdiDeployment.getUpdate().getName()).isEqualTo(ddiDeployment.getUpdate().getName());
assertThat(deserializedDdiDeployment.getMaintenanceWindow().getStatus()).isEqualTo(
ddiDeployment.getMaintenanceWindow().getStatus());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiDeployment = "{\"download\":\"forced\",\"update\":\"attempt\", "
+ "\"maintenanceWindow\":\"available\",\"chunks\":[],\"unknownProperty\":\"test\"}";
// Test
DdiDeployment ddiDeployment = mapper.readValue(serializedDdiDeployment, DdiDeployment.class);
assertThat(ddiDeployment.getDownload().getName()).isEqualTo(FORCED.getName());
assertThat(ddiDeployment.getUpdate().getName()).isEqualTo(ATTEMPT.getName());
assertThat(ddiDeployment.getMaintenanceWindow().getStatus()).isEqualTo(AVAILABLE.getStatus());
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiDeployment = "{\"download\":[\"forced\"],\"update\":\"attempt\", "
+ "\"maintenanceWindow\":\"available\",\"chunks\":[]}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiDeployment, DdiDeployment.class));
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiMetadata'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiMetadataTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
String key = "testKey";
String value = "testValue";
DdiMetadata ddiMetadata = new DdiMetadata(key, value);
// Test
String serializedDdiMetadata = mapper.writeValueAsString(ddiMetadata);
DdiMetadata deserializedDdiMetadata = mapper.readValue(serializedDdiMetadata, DdiMetadata.class);
assertThat(serializedDdiMetadata).contains(key, value);
assertThat(deserializedDdiMetadata.getKey()).isEqualTo(ddiMetadata.getKey());
assertThat(deserializedDdiMetadata.getValue()).isEqualTo(ddiMetadata.getValue());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiMetadata = "{\"key\":\"testKey\",\"value\":\"testValue\",\"unknownProperty\":\"test\"}";
// Test
DdiMetadata ddiMetadata = mapper.readValue(serializedDdiMetadata, DdiMetadata.class);
assertThat(ddiMetadata.getKey()).isEqualTo("testKey");
assertThat(ddiMetadata.getValue()).isEqualTo("testValue");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiMetadata = "{\"key\":[\"testKey\"],\"value\":\"testValue\"}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiMetadata, DdiMetadata.class));
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiPolling'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiPollingTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
DdiPolling ddiPolling = new DdiPolling("10");
// Test
String serializedDdiPolling = mapper.writeValueAsString(ddiPolling);
DdiPolling deserializedDdiPolling = mapper.readValue(serializedDdiPolling, DdiPolling.class);
assertThat(serializedDdiPolling).contains(ddiPolling.getSleep());
assertThat(deserializedDdiPolling.getSleep()).isEqualTo(ddiPolling.getSleep());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiPolling = "{\"sleep\":\"10\",\"unknownProperty\":\"test\"}";
// Test
DdiPolling ddiPolling = mapper.readValue(serializedDdiPolling, DdiPolling.class);
assertThat(ddiPolling.getSleep()).isEqualTo("10");
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiPolling = "{\"sleep\":[\"10\"]}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiPolling, DdiPolling.class));
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiProgress'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiProgressTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
DdiProgress ddiProgress = new DdiProgress(30, 100);
// Test
String serializedDdiProgress = mapper.writeValueAsString(ddiProgress);
DdiProgress deserializedDdiProgress = mapper.readValue(serializedDdiProgress, DdiProgress.class);
assertThat(serializedDdiProgress).contains(ddiProgress.getCnt().toString(), ddiProgress.getOf().toString());
assertThat(deserializedDdiProgress.getCnt()).isEqualTo(ddiProgress.getCnt());
assertThat(deserializedDdiProgress.getOf()).isEqualTo(ddiProgress.getOf());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiProgress = "{\"cnt\":30,\"of\":100,\"unknownProperty\":\"test\"}";
// Test
DdiProgress ddiProgress = mapper.readValue(serializedDdiProgress, DdiProgress.class);
assertThat(ddiProgress.getCnt()).isEqualTo(30);
assertThat(ddiProgress.getOf()).isEqualTo(100);
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiProgress = "{\"cnt\":[30],\"of\":100}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiProgress, DdiProgress.class));
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult.NONE;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Test serializability of DDI api model 'DdiResult'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiResultTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
DdiProgress ddiProgress = new DdiProgress(30, 100);
DdiResult ddiResult = new DdiResult(NONE, ddiProgress);
// Test
String serializedDdiResult = mapper.writeValueAsString(ddiResult);
DdiResult deserializedDdiResult = mapper.readValue(serializedDdiResult, DdiResult.class);
assertThat(serializedDdiResult).contains(NONE.getName(), ddiProgress.getCnt().toString(),
ddiProgress.getOf().toString());
assertThat(deserializedDdiResult.getFinished()).isEqualTo(ddiResult.getFinished());
assertThat(deserializedDdiResult.getProgress().getCnt()).isEqualTo(ddiProgress.getCnt());
assertThat(deserializedDdiResult.getProgress().getOf()).isEqualTo(ddiProgress.getOf());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
String serializedDdiResult = "{\"finished\":\"none\",\"progress\":{\"cnt\":30,\"of\":100},\"unknownProperty\":\"test\"}";
// Test
DdiResult ddiResult = mapper.readValue(serializedDdiResult, DdiResult.class);
assertThat(ddiResult.getFinished()).isEqualTo(NONE);
assertThat(ddiResult.getProgress().getCnt()).isEqualTo(30);
assertThat(ddiResult.getProgress().getOf()).isEqualTo(100);
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiResult = "{\"finished\":[\"none\"],\"progress\":{\"cnt\":30,\"of\":100}}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiResult, DdiResult.class));
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult.NONE;
import static org.eclipse.hawkbit.ddi.json.model.DdiStatus.ExecutionStatus.PROCEEDING;
import java.io.IOException;
import java.util.Collections;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
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;
/**
* Test serializability of DDI api model 'DdiStatus'
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serializability of DDI api Models")
public class DdiStatusTest {
private ObjectMapper mapper = new ObjectMapper();
@ParameterizedTest
@MethodSource("ddiStatusPossibilities")
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject(final DdiResult ddiResult, final DdiStatus ddiStatus) throws IOException {
// Test
String serializedDdiStatus = mapper.writeValueAsString(ddiStatus);
DdiStatus deserializedDdiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
assertThat(serializedDdiStatus).contains(ddiStatus.getExecution().getName(), ddiResult.getFinished().getName(),
ddiResult.getProgress().getCnt().toString(), ddiResult.getProgress().getOf().toString());
assertThat(deserializedDdiStatus.getExecution()).isEqualTo(ddiStatus.getExecution());
assertThat(deserializedDdiStatus.getResult().getFinished()).isEqualTo(ddiStatus.getResult().getFinished());
assertThat(deserializedDdiStatus.getResult().getProgress().getCnt()).isEqualTo(
ddiStatus.getResult().getProgress().getCnt());
assertThat(deserializedDdiStatus.getResult().getProgress().getOf()).isEqualTo(
ddiStatus.getResult().getProgress().getOf());
assertThat(deserializedDdiStatus.getDetails()).isEqualTo(ddiStatus.getDetails());
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
final String serializedDdiStatus = "{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\","
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"details\":[],\"unknownProperty\":\"test\"}";
// Test
final DdiStatus ddiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
assertThat(ddiStatus.getExecution()).isEqualTo(PROCEEDING);
assertThat(ddiStatus.getCode()).isNull();
assertThat(ddiStatus.getResult().getFinished()).isEqualTo(NONE);
assertThat(ddiStatus.getResult().getProgress().getCnt()).isEqualTo(30);
assertThat(ddiStatus.getResult().getProgress().getOf()).isEqualTo(100);
}
@Test
@Description("Verify the correct deserialization of a model with a provided code (optional)")
public void shouldDeserializeObjectWithOptionalCode() throws IOException {
// Setup
String serializedDdiStatus = "{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\","
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"code\": 12,\"details\":[]}";
// Test
DdiStatus ddiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
assertThat(ddiStatus.getExecution()).isEqualTo(PROCEEDING);
assertThat(ddiStatus.getCode()).isEqualTo(12);
assertThat(ddiStatus.getResult().getFinished()).isEqualTo(NONE);
assertThat(ddiStatus.getResult().getProgress().getCnt()).isEqualTo(30);
assertThat(ddiStatus.getResult().getProgress().getOf()).isEqualTo(100);
}
@Test
@Description("Verify that deserialization fails for known properties with a wrong datatype")
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
String serializedDdiStatus = "{\"execution\":[\"proceeding\"],\"result\":{\"finished\":\"none\","
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"details\":[]}";
// Test
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiStatus, DdiStatus.class));
}
private static Stream<Arguments> ddiStatusPossibilities() {
final DdiProgress ddiProgress = new DdiProgress(30, 100);
final DdiResult ddiResult = new DdiResult(NONE, ddiProgress);
return Stream.of(
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, null, Collections.emptyList())),
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, null, Collections.singletonList("testMessage"))),
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, 12, Collections.emptyList())));
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2019 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.json.model;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
/**
* Check DDI api model classes for '@JsonIgnoreProperties' annotation
*/
@Feature("Unit Tests - Direct Device Integration API")
@Story("Serialization of DDI api Models")
public class JsonIgnorePropertiesAnnotationTest {
@Test
@Description("This test verifies that all model classes within the 'org.eclipse.hawkbit.ddi.json.model' package are annotated with '@JsonIgnoreProperties(ignoreUnknown = true)'")
public void shouldCheckAnnotationsForAllModelClasses() {
final String packageName = getClass().getPackage().getName();
try (final ScanResult scanResult = new ClassGraph().acceptPackages(packageName).scan()) {
final List<? extends Class<?>> matchingClasses = scanResult.getAllClasses()
.stream()
.filter(classInPackage -> classInPackage.getSimpleName().endsWith("Test") || classInPackage.isEnum())
.map(ClassInfo::loadClass)
.toList();
assertThat(matchingClasses).isNotEmpty();
matchingClasses.forEach(modelClass -> {
if (modelClass.getSimpleName().contains("Test") || modelClass.isEnum()) {
return;
}
final JsonIgnoreProperties annotation = modelClass.getAnnotation(JsonIgnoreProperties.class);
assertThat(annotation)
.describedAs(
"Annotation 'JsonIgnoreProperties' is missing for class: " + modelClass.getSimpleName())
.isNotNull();
assertThat(annotation.ignoreUnknown()).isTrue();
});
}
}
}

View File

@@ -0,0 +1,14 @@
# Eclipse.IoT hawkBit - DDI Resource
This is the server-side implementation of the hawkBit DDI API and the hawkBit DDI Download API that is used by devices
for communicating with the HawkBit Update Server through HTTP.
# Compile
#### Build hawkbit-ddi-resource
```
$ cd hawkbit/hawkbit-ddi-resource
$ mvn clean install
```

View File

@@ -0,0 +1,105 @@
<!--
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
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-ddi-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-ddi-resource</artifactId>
<name>hawkBit :: REST :: DDI Resources</name>
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-ddi-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-rest-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-rest-core</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-http-security</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-aspects</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

@@ -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 &lt;token&gt;"))))
.build();
}
}

View File

@@ -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();
});
}
}

View File

@@ -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()));
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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)));
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -0,0 +1,60 @@
# hawkBit DDI Server (EXPERIMENTAL!)
The hawkBit DDI Server is a standalone spring-boot application with an embedded servlet container. It should be started
with at least hawkbit-mgmt-server.
## On your own workstation
### Run
```bash
java -jar hawkbit-runtime/hawkbit-ddi-server/target/hawkbit-ddi-server-*-SNAPSHOT.jar
```
_(Note: you have to add the JDBC driver also to your class path if you intend to use another database than H2.)_
Or:
```bash
run org.eclipse.hawkbit.app.ddi.DDIStart
```
### Usage
The Management API can be accessed via http://localhost:8081/rest/v1
The root url http://localhost:8081 will redirect directly to the Swagger Management UI
### Clustering (Experimental!!!)
The micro-service instances are configured to communicate via Spring Cloud Bus. You could run multiple instances of any
micro-service but hawkbit-mgmt-server. Management server run some schedulers which shall not run simultaneously - e.g.
auto assignment checker and rollouts executor. To run multiple management server instances you shall do some extensions
of hawkbit to ensure that they wont run schedulers simultaneously or you shall configure all instances but one to do not
run schedulers!
## Optional Protostuff for Sprign cloud bus
The micro-service instances are configured to communicate via Spring Cloud Bus. Optionally, you could
use [Protostuff](https://github.com/protostuff/protostuff) based message payload serialization for improved performance.
**Note**: If Protostuff is enabled it shall be enabled on all microservices!
Add/Uncomment to/in your `application.properties` :
```properties
spring.cloud.stream.bindings.springCloudBusInput.content-type=application/binary+protostuff
spring.cloud.stream.bindings.springCloudBusOutput.content-type=application/binary+protostuff
```
Add to your `pom.xml` :
```xml
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
</dependency>
```

View File

@@ -0,0 +1,94 @@
<!--
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
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-ddi-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-ddi-server</artifactId>
<name>hawkBit :: Runtime :: DDI Server</name>
<properties>
<spring.app.class>org.eclipse.hawkbit.app.ddi.DDIStart</spring.app.class>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-boot-starter-ddi-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<!-- if run against mysql -->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<outputDirectory>${baseDir}</outputDirectory>
<mainClass>${spring.app.class}</mainClass>
<layout>JAR</layout>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>

View File

@@ -0,0 +1,56 @@
/**
* 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.app.ddi;
import org.eclipse.hawkbit.autoconfigure.security.EnableHawkbitManagedSecurityConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;
/**
* A {@link SpringBootApplication} annotated class with a main method to start.
* The minimal configuration for the stand alone hawkBit DDI server.
*/
@SpringBootApplication(scanBasePackages = "org.eclipse.hawkbit")
@EnableHawkbitManagedSecurityConfiguration
public class DDIStart {
/**
* Main method to start the spring-boot application.
*
* @param args the VM arguments.
*/
public static void main(final String[] args) {
SpringApplication.run(DDIStart.class, args);
}
@Controller
public static class RedirectController {
@GetMapping("/")
public RedirectView redirectToSwagger(
RedirectAttributes attributes) {
attributes.addFlashAttribute("flashAttribute", "redirectWithRedirectView");
attributes.addAttribute("attribute", "redirectWithRedirectView");
return new RedirectView("swagger-ui/index.html");
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true)
public static class MethodSecurityConfig {
}
}

View File

@@ -0,0 +1,19 @@
#
# Copyright (c) 2019 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
#
# This profile adds basic configurations for a DB2 DB usage.
# Keep in mind that you need the DB2 driver in your classpath on compile.
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
spring.jpa.database=DB2
spring.datasource.url=jdbc:db2://localhost:50000/hawkbit
spring.datasource.username=db2inst1
spring.datasource.password=db2inst1-pwd
spring.datasource.driverClassName=com.ibm.db2.jcc.DB2Driver

View File

@@ -0,0 +1,19 @@
#
# Copyright (c) 2018 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
#
# This profile adds basic configurations for a Microsoft SQL Server DB usage.
# Keep in mind that you need the SQL server driver in your classpath on compile.
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
spring.jpa.database=SQL_SERVER
spring.datasource.url=jdbc:sqlserver://localhost:1433;database=hawkbit
spring.datasource.username=SA
spring.datasource.password=
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver

View File

@@ -0,0 +1,19 @@
#
# 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
#
# This profile adds basic configurations for a MySQL DB usage.
# Keep in mind that you need the MariaDB driver in your classpath on compile.
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
spring.jpa.database=MYSQL
spring.datasource.url=jdbc:mariadb://localhost:3306/hawkbit
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driverClassName=org.mariadb.jdbc.Driver

View File

@@ -0,0 +1,19 @@
#
# Copyright (c) 2020 Enapter Co.,Ltd
#
# 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
#
# This profile adds basic configurations for a PostgreSQL usage.
# Keep in mind that you need the PostgreSQL driver in your classpath on compile.
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
spring.jpa.database=POSTGRESQL
spring.datasource.url=jdbc:postgresql://localhost:5432/hawkbit
spring.datasource.username=postgres
spring.datasource.password=admin
spring.datasource.driverClassName=org.postgresql.Driver

View File

@@ -0,0 +1,83 @@
#
# 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
#
# Spring configuration
spring.application.name=ddi-server
spring.main.allow-bean-definition-overriding=true
server.port=8081
# Logging configuration
logging.level.org.eclipse.hawkbit.eventbus.DeadEventListener=WARN
logging.level.org.springframework.boot.actuate.audit.listener.AuditListener=WARN
logging.level.org.hibernate.validator.internal.util.Version=WARN
# security Log with hints on potential attacks
logging.level.server-security=INFO
# logging pattern
logging.pattern.console=%clr(%d{${logging.pattern.dateformat:yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${logging.pattern.level:%5p}) %clr(${PID:}){magenta} %clr(---){faint} %clr([${spring.application.name}] [%X{tenant}:%X{user}] [%15.15t]){faint} %clr(${logging.pattern.correlation:}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${logging.exception-conversion-word:%wEx}
# User Security
spring.security.user.name=admin
spring.security.user.password={noop}admin
# Http Encoding
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
# DDI authentication configuration
hawkbit.server.ddi.security.authentication.anonymous.enabled=false
hawkbit.server.ddi.security.authentication.targettoken.enabled=false
hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false
# Optional events
hawkbit.server.repository.publish-target-poll-event=false
## Configuration for DMF/RabbitMQ integration
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
# Enable CORS and specify the allowed origins:
#hawkbit.server.security.cors.enabled=true
#hawkbit.server.security.cors.allowedOrigins=http://localhost
# Swagger Configuration
springdoc.api-docs.version=openapi_3_0
springdoc.show-oauth2-endpoints=true
springdoc.show-login-endpoint=true
springdoc.packages-to-scan=org.eclipse.hawkbit.ddi
springdoc.paths-to-exclude=/system/**
# Flyway disabled - US only
spring.flyway.enabled=false
## SQL Database Configuration - END
## No Schedulers - START
hawkbit.autoassign.scheduler.enabled=false
hawkbit.rollout.scheduler.enabled=false
## No Schedulers - END
# Disable discovery client of spring-cloud-commons
spring.cloud.discovery.enabled=false
# Enable communication between services
spring.cloud.bus.enabled=true
spring.cloud.bus.ack.enabled=false
spring.cloud.bus.refresh.enabled=false
spring.cloud.bus.env.enabled=false
endpoints.spring.cloud.bus.refresh.enabled=false
endpoints.spring.cloud.bus.env.enabled=false
spring.cloud.stream.bindings.springCloudBusInput.group=ddi-server
# To use protostuff (for instance fot improved performance) you shall uncomment
# the following two lines and add io.protostuff:protostuff-core and io.protostuff:protostuff-runtime to dependencies
#spring.cloud.stream.bindings.springCloudBusInput.content-type=application/binary+protostuff
#spring.cloud.stream.bindings.springCloudBusOutput.content-type=application/binary+protostuff

View File

@@ -0,0 +1,14 @@
______ _ _ _ _ ____ _ _ _____ _____ _____
| ____| | (_) | | | | | _ \(_) | | __ \| __ \_ _|
| |__ ___| |_ _ __ ___ ___ | |__ __ ___ _| | _| |_) |_| |_ | | | | | | || |
| __| / __| | | '_ \/ __|/ _ \ | '_ \ / _` \ \ /\ / / |/ / _ <| | __| | | | | | | || |
| |___| (__| | | |_) \__ \ __/ | | | | (_| |\ V V /| <| |_) | | |_ | |__| | |__| || |_
|______\___|_|_| .__/|___/\___| |_| |_|\__,_| \_/\_/ |_|\_\____/|_|\__| |_____/|_____/_____|
| |
|_|
Eclipse hawkBit DDI Server ${application.formatted-version}
using Spring Boot ${spring-boot.formatted-version}
Go to https://www.eclipse.org/hawkbit for more information.

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2020 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.app.ddi;
import org.eclipse.hawkbit.repository.test.util.SharedSqlTestDatabaseExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
@ExtendWith(SharedSqlTestDatabaseExtension.class)
public abstract class AbstractSecurityTest {
protected MockMvc mvc;
@Autowired
private WebApplicationContext context;
@BeforeEach
public void setup() {
final DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity()).dispatchOptions(true);
mvc = builder.build();
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2019 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.app.ddi;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.TestPropertySource;
@TestPropertySource(properties = { "hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
@Feature("Integration Test - Security")
@Story("Allowed Host Names")
public class AllowedHostNamesTest extends AbstractSecurityTest {
@Test
@Description("Tests whether a RequestRejectedException is thrown when not allowed host is used")
public void allowedHostNameWithNotAllowedHost() throws Exception {
mvc.perform(get("/").header(HttpHeaders.HOST, "www.google.com")).andExpect(status().isBadRequest());
}
@Test
@Description("Tests whether request is redirected when allowed host is used")
public void allowedHostNameWithAllowedHost() throws Exception {
mvc.perform(get("/").header(HttpHeaders.HOST, "localhost")).andExpect(status().is3xxRedirection());
}
@Test
@Description("Tests whether request without allowed host name and with ignored path end up with a client error")
public void notAllowedHostnameWithIgnoredPath() throws Exception {
mvc.perform(get("/index.html").header(HttpHeaders.HOST, "www.google.com"))
.andExpect(status().is4xxClientError());
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.app.ddi;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
@Feature("Integration Test - Security")
@Story("PreAuthorized enabled")
@TestPropertySource(properties = { "spring.flyway.enabled=true" })
public class PreAuthorizeEnabledTest extends AbstractSecurityTest {
@Test
@Description("Tests whether request fail if a role is forbidden for the user")
@WithUser(authorities = { SpPermission.READ_TARGET })
public void failIfNoRole() throws Exception {
mvc.perform(get("/DEFAULT/controller/v1/controllerId")).andExpect(result ->
assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.FORBIDDEN.value()));
}
@Test
@Description("Tests whether request succeed if a role is granted for the user")
@WithUser(authorities = { SpPermission.SpringEvalExpressions.CONTROLLER_ROLE })
public void successIfHasRole() throws Exception {
mvc.perform(get("/DEFAULT/controller/v1/controllerId")).andExpect(result -> {
assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value());
});
}
}

31
hawkbit-ddi/pom.xml Normal file
View File

@@ -0,0 +1,31 @@
<!--
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
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-ddi-parent</artifactId>
<name>hawkBit :: DDI</name>
<packaging>pom</packaging>
<modules>
<module>hawkbit-ddi-api</module>
<module>hawkbit-ddi-resource</module>
<module>hawkbit-boot-starter-ddi-api</module>
<module>hawkbit-ddi-server</module>
</modules>
</project>