use std::rc::Rc;
use std::{
cell::{Cell, RefCell},
path::PathBuf,
};
use httpmock::{
Method::{GET, POST, PUT},
Mock, MockExt, MockServer,
};
use httpmock::{Then, When};
use serde_json::{json, Map, Value};
use hawkbit::ddi::{
ClientAuthorization, ConfirmationResponse, Execution, Finished, MaintenanceWindow, Type,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TargetAuthorization {
None,
TargetToken,
GatewayToken,
}
pub struct ServerBuilder {
tenant: String,
target_authorization: TargetAuthorization,
}
impl Default for ServerBuilder {
fn default() -> Self {
Self {
tenant: "DEFAULT".into(),
target_authorization: TargetAuthorization::TargetToken,
}
}
}
impl ServerBuilder {
pub fn tenant(self, tenant: &str) -> Self {
let mut builder = self;
builder.tenant = tenant.to_string();
builder
}
pub fn target_authorization(self, target_authorization: TargetAuthorization) -> Self {
let mut builder = self;
builder.target_authorization = target_authorization;
builder
}
pub fn build(self) -> Server {
Server {
server: Rc::new(MockServer::start()),
tenant: self.tenant,
target_authorization: self.target_authorization,
}
}
}
pub struct Server {
pub tenant: String,
pub target_authorization: TargetAuthorization,
server: Rc<MockServer>,
}
impl Server {
pub fn base_url(&self) -> String {
self.server.base_url()
}
pub fn add_target(&self, name: &str) -> Target {
let client_auth = match self.target_authorization {
TargetAuthorization::None => ClientAuthorization::None,
TargetAuthorization::TargetToken => {
ClientAuthorization::TargetToken(format!("Key{}", name))
}
TargetAuthorization::GatewayToken => {
ClientAuthorization::GatewayToken(format!("Key{}", self.tenant))
}
};
Target::new(name, &self.server, &self.tenant, &client_auth)
}
}
pub struct Target {
pub name: String,
pub client_auth: ClientAuthorization,
server: Rc<MockServer>,
tenant: String,
poll: Cell<usize>,
config_data: RefCell<Option<PendingAction>>,
confirmation: RefCell<Option<PendingAction>>,
deployment: RefCell<Option<PendingAction>>,
cancel_action: RefCell<Option<PendingAction>>,
}
impl Target {
fn new(
name: &str,
server: &Rc<MockServer>,
tenant: &str,
client_auth: &ClientAuthorization,
) -> Self {
let poll = Self::create_poll(server, tenant, name, client_auth, None, None, None, None);
Target {
name: name.to_string(),
client_auth: client_auth.clone(),
server: server.clone(),
tenant: tenant.to_string(),
poll: Cell::new(poll),
config_data: RefCell::new(None),
confirmation: RefCell::new(None),
deployment: RefCell::new(None),
cancel_action: RefCell::new(None),
}
}
#[allow(clippy::too_many_arguments)]
fn create_poll(
server: &MockServer,
tenant: &str,
name: &str,
client_auth: &ClientAuthorization,
expected_config_data: Option<&PendingAction>,
confirmation: Option<&PendingAction>,
deployment: Option<&PendingAction>,
cancel_action: Option<&PendingAction>,
) -> usize {
let mut links = Map::new();
if let Some(pending) = expected_config_data {
links.insert("configData".into(), json!({ "href": pending.path }));
}
if let Some(pending) = confirmation {
links.insert("confirmationBase".into(), json!({ "href": pending.path }));
}
if let Some(pending) = deployment {
links.insert("deploymentBase".into(), json!({ "href": pending.path }));
}
if let Some(pending) = cancel_action {
links.insert("cancelAction".into(), json!({ "href": pending.path }));
}
let response = json!({
"config": {
"polling": {
"sleep": "00:01:00"
}
},
"_links": links
});
let mock = server.mock(|when, then| {
let when = when
.method(GET)
.path(format!("/{}/controller/v1/{}", tenant, name));
match client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200)
.header("Content-Type", "application/json")
.json_body(response);
});
mock.id()
}
fn update_poll(&self) {
let old = self.poll.replace(Self::create_poll(
&self.server,
&self.tenant,
&self.name,
&self.client_auth,
self.config_data.borrow().as_ref(),
self.confirmation.borrow().as_ref(),
self.deployment.borrow().as_ref(),
self.cancel_action.borrow().as_ref(),
));
let mut old = Mock::new(old, &self.server);
old.delete();
}
pub fn request_config(&self, expected_config_data: Value) {
let config_path = self
.server
.url(format!("/DEFAULT/controller/v1/{}/configData", self.name));
let config_data = self.server.mock(|when, then| {
let when = when
.method(PUT)
.path(format!("/DEFAULT/controller/v1/{}/configData", self.name))
.header("Content-Type", "application/json")
.json_body(expected_config_data);
match &self.client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200);
});
self.config_data.replace(Some(PendingAction {
server: self.server.clone(),
path: config_path,
mock: config_data.id(),
}));
self.update_poll();
}
pub fn push_deployment(&self, deploy: Deployment) {
if deploy.confirmation_required {
let confirmation_path = self.server.url(format!(
"/DEFAULT/controller/v1/{}/confirmationBase/{}",
self.name, deploy.id
));
let base_url = self.server.url("/download");
let response = deploy.json(&base_url);
let confirmation_mock = self.server.mock(|when, then| {
let when = when.method(GET).path(format!(
"/DEFAULT/controller/v1/{}/confirmationBase/{}",
self.name, deploy.id
));
match &self.client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200)
.header("Content-Type", "application/json")
.json_body(response);
});
self.deployment.replace(None);
self.confirmation.replace(Some(PendingAction {
server: self.server.clone(),
path: confirmation_path,
mock: confirmation_mock.id(),
}));
} else {
let deploy_path = self.server.url(format!(
"/DEFAULT/controller/v1/{}/deploymentBase/{}",
self.name, deploy.id
));
let base_url = self.server.url("/download");
let response = deploy.json(&base_url);
let deploy_mock = self.server.mock(|when, then| {
let when = when.method(GET).path(format!(
"/DEFAULT/controller/v1/{}/deploymentBase/{}",
self.name, deploy.id
));
match &self.client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200)
.header("Content-Type", "application/json")
.json_body(response);
});
for chunk in deploy.chunks.iter() {
for (artifact, _md5, _sha1, _sha256) in chunk.artifacts.iter() {
let file_name = artifact.file_name().unwrap().to_str().unwrap();
let path = format!("/download/{}", file_name);
if let Some(mock_fn) = &chunk.mock {
self.server.mock(mock_fn);
} else {
self.server.mock(|when, then| {
let when = when.method(GET).path(path);
match &self.client_auth {
ClientAuthorization::None => {
}
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200).body_from_file(artifact.to_str().unwrap());
});
}
}
}
self.confirmation.replace(None);
self.deployment.replace(Some(PendingAction {
server: self.server.clone(),
path: deploy_path,
mock: deploy_mock.id(),
}));
}
self.update_poll();
}
pub fn expect_deployment_feedback(
&self,
deployment_id: &str,
execution: Execution,
finished: Finished,
progress: Option<serde_json::Value>,
details: Vec<&str>,
) -> Mock<'_> {
self.server.mock(|when, then| {
let expected = match progress {
Some(progress) => json!({
"id": deployment_id,
"status": {
"result": {
"progress": progress,
"finished": finished
},
"execution": execution,
"details": details,
},
}),
None => json!({
"id": deployment_id,
"status": {
"result": {
"finished": finished
},
"execution": execution,
"details": details,
},
}),
};
let when = when
.method(POST)
.path(format!(
"/{}/controller/v1/{}/deploymentBase/{}/feedback",
self.tenant, self.name, deployment_id
))
.header("Content-Type", "application/json")
.json_body(expected);
match &self.client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200);
})
}
pub fn expect_confirmation_feedback(
&self,
deployment_id: &str,
code: Option<i32>,
confirmation: ConfirmationResponse,
details: Vec<&str>,
) -> Mock<'_> {
self.server.mock(|when, then| {
let expected = match code {
Some(code) => json!({
"confirmation": confirmation,
"code": code,
"details": details,
}),
None => json!({
"confirmation": confirmation,
"details": details,
}),
};
let when = when
.method(POST)
.path(format!(
"/{}/controller/v1/{}/confirmationBase/{}/feedback",
self.tenant, self.name, deployment_id
))
.header("Content-Type", "application/json")
.json_body(expected);
match &self.client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200);
})
}
pub fn cancel_action(&self, id: &str) {
let cancel_path = self.server.url(format!(
"/DEFAULT/controller/v1/{}/cancelAction/{}",
self.name, id
));
let response = json!({
"id": id,
"cancelAction": {
"stopId": id
}
});
let cancel_mock = self.server.mock(|when, then| {
let when = when.method(GET).path(format!(
"/DEFAULT/controller/v1/{}/cancelAction/{}",
self.name, id
));
match &self.client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200)
.header("Content-Type", "application/json")
.json_body(response);
});
self.cancel_action.replace(Some(PendingAction {
server: self.server.clone(),
path: cancel_path,
mock: cancel_mock.id(),
}));
self.update_poll();
}
pub fn expect_cancel_feedback(
&self,
cancel_id: &str,
execution: Execution,
finished: Finished,
details: Vec<&str>,
) -> Mock<'_> {
self.server.mock(|when, then| {
let expected = json!({
"id": cancel_id,
"status": {
"result": {
"finished": finished
},
"execution": execution,
"details": details,
},
});
let when = when
.method(POST)
.path(format!(
"/{}/controller/v1/{}/cancelAction/{}/feedback",
self.tenant, self.name, cancel_id
))
.header("Content-Type", "application/json")
.json_body(expected);
match &self.client_auth {
ClientAuthorization::None => { }
ClientAuthorization::TargetToken(key) => {
when.header("Authorization", format!("TargetToken {}", key));
}
ClientAuthorization::GatewayToken(key) => {
when.header("Authorization", format!("GatewayToken {}", key));
}
};
then.status(200);
})
}
pub fn poll_hits(&self) -> usize {
let mock = Mock::new(self.poll.get(), &self.server);
mock.calls()
}
pub fn config_data_hits(&self) -> usize {
self.config_data.borrow().as_ref().map_or(0, |m| {
let mock = Mock::new(m.mock, &self.server);
mock.calls()
})
}
pub fn deployment_hits(&self) -> usize {
self.deployment.borrow().as_ref().map_or(0, |m| {
let mock = Mock::new(m.mock, &self.server);
mock.calls()
})
}
pub fn confirmation_hits(&self) -> usize {
self.confirmation.borrow().as_ref().map_or(0, |m| {
let mock = Mock::new(m.mock, &self.server);
mock.calls()
})
}
pub fn cancel_action_hits(&self) -> usize {
self.cancel_action.borrow().as_ref().map_or(0, |m| {
let mock = Mock::new(m.mock, &self.server);
mock.calls()
})
}
}
struct PendingAction {
server: Rc<MockServer>,
mock: usize,
path: String,
}
impl Drop for PendingAction {
fn drop(&mut self) {
let mut mock = Mock::new(self.mock, &self.server);
mock.delete();
}
}
pub struct DeploymentBuilder {
id: String,
confirmation_required: bool,
download_type: Type,
update_type: Type,
maintenance_window: Option<MaintenanceWindow>,
chunks: Vec<Chunk>,
}
pub struct Deployment {
pub id: String,
confirmation_required: bool,
download_type: Type,
update_type: Type,
maintenance_window: Option<MaintenanceWindow>,
chunks: Vec<Chunk>,
}
impl DeploymentBuilder {
pub fn new(id: &str, download_type: Type, update_type: Type) -> Self {
Self {
id: id.to_string(),
confirmation_required: false,
download_type,
update_type,
maintenance_window: None,
chunks: Vec::new(),
}
}
pub fn confirmation_required(self, confirmation_required: bool) -> Self {
let mut builder = self;
builder.confirmation_required = confirmation_required;
builder
}
pub fn maintenance_window(self, maintenance_window: MaintenanceWindow) -> Self {
let mut builder = self;
builder.maintenance_window = Some(maintenance_window);
builder
}
pub fn chunk(
self,
protocol: ChunkProtocol,
part: &str,
version: &str,
name: &str,
artifacts: Vec<(PathBuf, &str, &str, &str)>,
) -> Self {
let mut builder = self;
let artifacts = artifacts
.into_iter()
.map(|(path, md5, sha1, sha256)| {
assert!(path.exists());
(path, md5.to_string(), sha1.to_string(), sha256.to_string())
})
.collect();
let chunk = Chunk {
protocol,
part: part.to_string(),
version: version.to_string(),
name: name.to_string(),
artifacts,
mock: None,
metadata: None,
};
builder.chunks.push(chunk);
builder
}
pub fn chunk_with_metadata(
self,
protocol: ChunkProtocol,
part: &str,
version: &str,
name: &str,
artifacts: Vec<(PathBuf, &str, &str, &str)>,
metadata: Vec<(String, String)>,
) -> Self {
let mut builder = self;
let artifacts = artifacts
.into_iter()
.map(|(path, md5, sha1, sha256)| {
assert!(path.exists());
(path, md5.to_string(), sha1.to_string(), sha256.to_string())
})
.collect();
let chunk = Chunk {
protocol,
part: part.to_string(),
version: version.to_string(),
name: name.to_string(),
artifacts,
metadata: Some(
metadata
.into_iter()
.map(|(key, value)| Metadata { key, value })
.collect(),
),
mock: None,
};
builder.chunks.push(chunk);
builder
}
pub fn chunk_with_mock(
self,
protocol: ChunkProtocol,
part: &str,
version: &str,
name: &str,
artifacts: Vec<(PathBuf, &str, &str, &str)>,
mock: Box<dyn Fn(When, Then)>,
) -> Self {
let mut builder = self;
let artifacts = artifacts
.into_iter()
.map(|(path, md5, sha1, sha256)| {
assert!(path.exists());
(path, md5.to_string(), sha1.to_string(), sha256.to_string())
})
.collect();
let chunk = Chunk {
protocol,
part: part.to_string(),
version: version.to_string(),
name: name.to_string(),
artifacts,
mock: Some(mock),
metadata: None,
};
builder.chunks.push(chunk);
builder
}
pub fn build(self) -> Deployment {
Deployment {
id: self.id,
confirmation_required: self.confirmation_required,
download_type: self.download_type,
update_type: self.update_type,
maintenance_window: self.maintenance_window,
chunks: self.chunks,
}
}
}
pub enum ChunkProtocol {
BOTH,
HTTP,
HTTPS,
}
impl ChunkProtocol {
pub fn http(&self) -> bool {
matches!(self, Self::BOTH | Self::HTTP)
}
pub fn https(&self) -> bool {
matches!(self, Self::BOTH | Self::HTTPS)
}
}
pub struct Metadata {
key: String,
value: String,
}
impl Metadata {
fn json(&self) -> serde_json::Value {
json!({
"key": self.key,
"value": self.value,
})
}
}
pub struct Chunk {
protocol: ChunkProtocol,
part: String,
version: String,
name: String,
artifacts: Vec<(PathBuf, String, String, String)>, metadata: Option<Vec<Metadata>>,
mock: Option<Box<dyn Fn(When, Then)>>,
}
impl Chunk {
fn json(&self, base_url: &str) -> serde_json::Value {
let artifacts: Vec<serde_json::Value> = self
.artifacts
.iter()
.map(|(path, md5, sha1, sha256)| {
let meta = path.metadata().unwrap();
let file_name = path.file_name().unwrap().to_str().unwrap();
let download_url = format!("{}/{}", base_url, file_name);
let md5_url = format!("{}.MD5SUM", download_url);
let mut links = serde_json::Map::new();
if self.protocol.https() {
links.insert("download".to_string(), json!({ "href": download_url }));
links.insert("md5sum".to_string(), json!({ "href": md5_url }));
}
if self.protocol.http() {
links.insert("download-http".to_string(), json!({ "href": download_url }));
links.insert("md5sum-http".to_string(), json!({ "href": md5_url }));
}
json!({
"filename": file_name,
"hashes": {
"sha1": sha1,
"md5": md5,
"sha256": sha256,
},
"size": meta.len(),
"_links": links,
})
})
.collect();
let mut result = json!({
"part": self.part,
"version": self.version,
"name": self.name,
"artifacts": artifacts,
});
if let Some(metadata) = &self.metadata {
let metadata: Vec<serde_json::Value> = metadata.iter().map(|m| m.json()).collect();
result
.as_object_mut()
.unwrap()
.insert("metadata".to_string(), json!(metadata));
}
result
}
}
impl Deployment {
fn json(&self, base_url: &str) -> serde_json::Value {
let chunks: Vec<serde_json::Value> = self.chunks.iter().map(|c| c.json(base_url)).collect();
let mut j = if self.confirmation_required {
json!({
"id": self.id,
"confirmation": {
"download": self.download_type,
"update": self.update_type,
"chunks": chunks,
}
})
} else {
json!({
"id": self.id,
"deployment": {
"download": self.download_type,
"update": self.update_type,
"chunks": chunks,
}
})
};
if let Some(maintenance_window) = &self.maintenance_window {
let d = j
.get_mut(if self.confirmation_required {
"confirmation"
} else {
"deployment"
})
.unwrap()
.as_object_mut()
.unwrap();
d.insert("maintenanceWindow".to_string(), json!(maintenance_window));
}
j
}
}