Lines
99.51 %
Functions
100 %
Branches
// Copyright 2020, Collabora Ltd.
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::fs::File;
use std::io::prelude::*;
use std::{path::PathBuf, time::Duration};
use bytes::Bytes;
use futures::prelude::*;
use hawkbit::ddi::{
Client, ConfirmationResponse, Error, Execution, Finished, MaintenanceWindow, Mode, Type,
};
use httpmock::Method::GET;
use httpmock::{Then, When};
use serde::Serialize;
use serde_json::json;
use tempdir::TempDir;
use hawkbit_mock::ddi::{
ChunkProtocol, Deployment, DeploymentBuilder, Server, ServerBuilder, Target,
fn init() {
let _ = env_logger::builder().is_test(true).try_init();
}
fn add_target(server: &Server, name: &str) -> (Client, Target) {
let target = server.add_target(name);
let client = Client::new(
&server.base_url(),
&server.tenant,
&target.name,
target.client_auth.clone(),
None,
)
.expect("DDI creation failed");
(client, target)
#[tokio::test]
async fn poll() {
init();
let server = ServerBuilder::default().tenant("my-tenant").build();
let (client, target) = add_target(&server, "Target1");
assert_eq!(target.poll_hits(), 0);
// Try polling twice
for i in 0..2 {
let reply = client.poll().await.expect("poll failed");
assert_eq!(reply.polling_sleep().unwrap(), Duration::from_secs(60));
assert!(reply.config_data_request().is_none());
assert!(reply.update().is_none());
assert_eq!(target.poll_hits(), i + 1);
async fn upload_config() {
let server = ServerBuilder::default().build();
let expected_config_data = json!({
"mode" : "merge",
"data" : {
"awesome" : true,
},
"status" : {
"result" : {
"finished" : "success"
"execution" : "closed",
"details" : [ "Some stuffs" ]
});
target.request_config(expected_config_data);
let config_data_req = reply
.config_data_request()
.expect("missing config data request");
#[derive(Serialize)]
struct Config {
awesome: bool,
let config = Config { awesome: true };
config_data_req
.upload(
Execution::Closed,
Finished::Success,
Some(Mode::Merge),
config,
vec!["Some stuffs"],
.await
.expect("upload config failed");
assert_eq!(target.poll_hits(), 1);
assert_eq!(target.config_data_hits(), 1);
fn artifact_path() -> PathBuf {
let mut test_artifact = PathBuf::new();
test_artifact.push("tests");
test_artifact.push("data");
test_artifact.push("test.txt");
test_artifact
fn get_deployment(needs_confirmation: bool, valid_checksums: bool) -> Deployment {
let test_artifact = artifact_path();
let artifacts = if valid_checksums {
vec![(
test_artifact,
"5eb63bbbe01eeed093cb22bb8f5acdc3",
"2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
)]
} else {
vec![(test_artifact, "badger", "badger", "badger")]
DeploymentBuilder::new("10", Type::Forced, Type::Attempt)
.confirmation_required(needs_confirmation)
.maintenance_window(MaintenanceWindow::Available)
.chunk(
ChunkProtocol::BOTH,
"app-both",
"1.0",
"some-chunk",
artifacts.clone(),
.chunk_with_metadata(
ChunkProtocol::HTTP,
"app-http",
vec![("key1".to_string(), "value1".to_string())],
ChunkProtocol::HTTPS,
"app-https",
artifacts,
vec![
("key2".to_string(), "value2".to_string()),
("key3".to_string(), "value3".to_string()),
],
.build()
async fn deployment() {
target.push_deployment(get_deployment(false, true));
assert_eq!(target.deployment_hits(), 0);
let update = reply.update().expect("missing update");
let update = update.fetch().await.expect("failed to fetch update info");
assert_eq!(target.deployment_hits(), 1);
assert_eq!(update.action_id(), "10");
assert_eq!(update.download_type(), Type::Forced);
assert_eq!(update.update_type(), Type::Attempt);
assert_eq!(
update.maintenance_window(),
Some(MaintenanceWindow::Available)
);
assert_eq!(update.chunks().count(), 3);
let mut chunks = update.chunks();
for p in &[
] {
// Check chunk
let chunk = chunks.next().unwrap();
let name = match p {
ChunkProtocol::BOTH => "app-both",
ChunkProtocol::HTTP => "app-http",
ChunkProtocol::HTTPS => "app-https",
assert_eq!(chunk.part(), name);
assert_eq!(chunk.version(), "1.0");
assert_eq!(chunk.name(), "some-chunk");
assert_eq!(chunk.artifacts().count(), 1);
let art = chunk.artifacts().next().unwrap();
assert_eq!(art.filename(), "test.txt");
assert_eq!(art.size(), 11);
let out_dir = TempDir::new("test-hawkbitrs").expect("Failed to create temp dir");
let artifacts = chunk
.download(out_dir.path())
.expect("Failed to download update");
// Check artifact
assert_eq!(artifacts.len(), 1);
let p = artifacts[0].file();
assert_eq!(p.file_name().unwrap(), "test.txt");
assert!(p.exists());
#[cfg(feature = "hash-md5")]
artifacts[0].check_md5().await.expect("invalid md5");
#[cfg(feature = "hash-sha1")]
artifacts[0].check_sha1().await.expect("invalid sha1");
#[cfg(feature = "hash-sha256")]
artifacts[0].check_sha256().await.expect("invalid sha256");
async fn resume_download() {
let artifacts = vec![(
)];
let deployment = DeploymentBuilder::new("10", Type::Forced, Type::Attempt)
.confirmation_required(false)
.chunk_with_mock(
// Modify the artifact to be a partial download
Box::new(|when: When, then: Then| {
let when = when.method(GET).path("/download/test.txt");
when.header("Range", "bytes=5-");
then.status(206).body(" world");
}),
.build();
target.push_deployment(deployment);
let mut partial_download_file = out_dir.path().to_path_buf();
partial_download_file.push("some-chunk");
partial_download_file.push("test.txt.part");
// Write a wrong prefix to the part file to be able to test whether
// the download is resumed or simply overwritten.
tokio::fs::create_dir_all(partial_download_file.parent().unwrap())
.unwrap();
tokio::fs::write(partial_download_file, b"HELLO")
assert_eq!(tokio::fs::read_to_string(p).await.unwrap(), "HELLO world");
async fn send_deployment_feedback() {
let deploy = get_deployment(false, true);
let deploy_id = deploy.id.clone();
target.push_deployment(deploy);
// Send feedback without progress
let mut mock = target.expect_deployment_feedback(
&deploy_id,
Execution::Proceeding,
Finished::None,
vec!["Downloading"],
assert_eq!(mock.calls(), 0);
update
.send_feedback(Execution::Proceeding, Finished::None, vec!["Downloading"])
.expect("Failed to send feedback");
assert_eq!(mock.calls(), 1);
mock.delete();
// Send feedback with progress
Some(json!({"awesome": true})),
vec!["Done"],
struct Progress {
let progress = Progress { awesome: true };
.send_feedback_with_progress(
Some(progress),
async fn confirmation() {
let deploy = get_deployment(true, true);
let confirmation = reply
.confirmation_base()
.expect("missing confirmation request");
// Decline the confirmation
let mut mock = target.expect_confirmation_feedback(
Some(-1),
ConfirmationResponse::Denied,
vec![],
confirmation
.decline()
let update_info = confirmation
.update_info()
.expect("failed to fetch update info");
let action_id = update_info.action_id();
assert_eq!(action_id, deploy_id);
update_info.metadata(),
confirmation.metadata().await.unwrap()
// Accept the confirmation
Some(1),
ConfirmationResponse::Confirmed,
.confirm()
async fn confirmation_metadata() {
let metadata = confirmation
.metadata()
.expect("failed to fetch metadata");
assert_eq!(metadata.len(), 3);
assert_eq!(metadata[0], ("key1".to_string(), "value1".to_string()));
assert_eq!(metadata[1], ("key2".to_string(), "value2".to_string()));
assert_eq!(metadata[2], ("key3".to_string(), "value3".to_string()));
async fn config_then_deploy() {
// server requests config
assert!(reply.config_data_request().is_some());
// server pushes an update
assert!(reply.update().is_some());
async fn download_stream() {
let chunk = update.chunks().next().unwrap();
async fn check_download(mut stream: Box<dyn Stream<Item = Result<Bytes, Error>> + Unpin>) {
let mut downloaded: Vec<u8> = Vec::new();
while let Some(b) = stream.next().await {
downloaded.extend(b.unwrap().as_ref());
// Compare downloaded content with the actual file
let mut art_file = File::open(&artifact_path()).expect("failed to open artifact");
let mut expected = Vec::new();
art_file
.read_to_end(&mut expected)
.expect("failed to read artifact");
assert_eq!(downloaded, expected);
// Download artifact using the stream API
let stream = art
.download_stream()
.expect("failed to get download stream");
check_download(Box::new(stream)).await;
cfg_if::cfg_if! {
if #[cfg(feature = "hash-md5")] {
.download_stream_with_md5_check()
if #[cfg(feature = "hash-sha1")] {
.download_stream_with_sha1_check()
if #[cfg(feature = "hash-sha256")] {
.download_stream_with_sha256_check()
#[cfg(feature = "hash-digest")]
async fn wrong_checksums() {
use assert_matches::assert_matches;
use hawkbit::ddi::ChecksumType;
target.push_deployment(get_deployment(false, false));
let downloaded = art
.expect("failed to download artifact");
assert_matches!(
downloaded.check_md5().await,
Err(Error::ChecksumError(ChecksumType::Md5))
downloaded.check_sha1().await,
Err(Error::ChecksumError(ChecksumType::Sha1))
downloaded.check_sha256().await,
Err(Error::ChecksumError(ChecksumType::Sha256))
let end = stream.skip_while(|b| future::ready(b.is_ok())).next().await;
assert_matches!(end, Some(Err(Error::ChecksumError(ChecksumType::Md5))));
assert_matches!(end, Some(Err(Error::ChecksumError(ChecksumType::Sha1))));
assert_matches!(end, Some(Err(Error::ChecksumError(ChecksumType::Sha256))));
async fn cancel_action() {
target.cancel_action("10");
let cancel_action = reply.cancel_action().expect("missing cancel action");
let id = cancel_action
.id()
.expect("failed to fetch cancel action id");
assert_eq!(id, "10");
assert_eq!(target.cancel_action_hits(), 1);
let mut mock = target.expect_cancel_feedback(
&id,
vec!["Cancelling"],
cancel_action
.send_feedback(Execution::Proceeding, Finished::None, vec!["Cancelling"])
async fn client_authorization() {
let server = ServerBuilder::default()
.target_authorization(hawkbit_mock::ddi::TargetAuthorization::None)
let (client, _target) = add_target(&server, "Target1");
client.poll().await.expect("poll failed");
.target_authorization(hawkbit_mock::ddi::TargetAuthorization::TargetToken)
let _ = add_target(&server, "Target2");
// create the client manually to control the authorization method
let client1 = Client::new(
"Target2",
hawkbit::ddi::ClientAuthorization::TargetToken("KeyTarget2".to_string()),
let client2 = Client::new(
hawkbit::ddi::ClientAuthorization::TargetToken("WrongToken".to_string()),
let client3 = Client::new(
hawkbit::ddi::ClientAuthorization::None,
client1.poll().await.expect("poll failed");
client2
.poll()
.expect_err("poll with wrong token succeeded");
client3
.expect_err("poll without token succeeded");
.target_authorization(hawkbit_mock::ddi::TargetAuthorization::GatewayToken)
let _ = add_target(&server, "Target3");
"Target3",
hawkbit::ddi::ClientAuthorization::GatewayToken("KeyDEFAULT".to_string()),
hawkbit::ddi::ClientAuthorization::GatewayToken("WrongToken".to_string()),
hawkbit::ddi::ClientAuthorization::TargetToken("KeyTarget3".to_string()),
.expect_err("poll with TargetToken succeeded");
async fn certificate() {
// the mock server used by hawkbit_mock does not support https, so we
// cannot test it fully, but we can at least test loading a certificate
// file.
let (_client, _target) = add_target(&server, "Target1");
"Target1",
hawkbit::ddi::ClientAuthorization::TargetToken("KeyTarget1".to_string()),
Some("tests/data/test.txt"),
// this returns an error, as the mock server does not use https
client1.poll().await.unwrap_err();