summaryrefslogtreecommitdiffstats
path: root/crates/core/c8y_api
diff options
context:
space:
mode:
authorinitard <solo@softwareag.com>2022-02-01 17:49:53 +0100
committerinitard <solo@softwareag.com>2022-02-15 11:43:49 +0000
commit2411743f17e14fa6031d4940d6a0135abdc00baf (patch)
treeddc7782f8e72a4a3b4531585ec93779bfaf0c05c /crates/core/c8y_api
parentae402f67527d022a3cedd60f049d73724079850a (diff)
c8y_api lib, tedge config, mqtt/http utilities (#790)
Preparing the repo for the log request plugin. Restructuring folders, moving code out of sm_c8y_mapper and into c8y_api, c8y_smartrest or tedge_config. Signed-off-by: initard <solo@softwareag.com>
Diffstat (limited to 'crates/core/c8y_api')
-rw-r--r--crates/core/c8y_api/Cargo.toml38
-rw-r--r--crates/core/c8y_api/src/error.rs1
-rw-r--r--crates/core/c8y_api/src/http_proxy.rs421
-rw-r--r--crates/core/c8y_api/src/json_c8y.rs282
-rw-r--r--crates/core/c8y_api/src/lib.rs12
5 files changed, 754 insertions, 0 deletions
diff --git a/crates/core/c8y_api/Cargo.toml b/crates/core/c8y_api/Cargo.toml
new file mode 100644
index 00000000..1e3cb0f5
--- /dev/null
+++ b/crates/core/c8y_api/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "c8y_api"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+agent_interface = { path = "../agent_interface"}
+async-trait = "0.1"
+batcher = { path = "../../common/batcher" }
+c8y_smartrest = { path = "../c8y_smartrest" }
+c8y_translator = { path = "../c8y_translator" }
+chrono = "0.4"
+clock = { path = "../../common/clock" }
+csv = "1.1"
+download = { path = "../../common/download" }
+flockfile = { path = "../../common/flockfile" }
+futures = "0.3"
+mockall = "0.10"
+mqtt_channel = { path = "../../common/mqtt_channel" }
+reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+structopt = "0.3"
+tedge_config = { path = "../../common/tedge_config" }
+tedge_users = { path = "../../common/tedge_users" }
+tedge_utils = { path = "../../common/tedge_utils", features = ["logging"] }
+thin_edge_json = { path = "../thin_edge_json" }
+thiserror = "1.0"
+time = { version = "0.3", features = ["formatting"] }
+tokio = { version = "1.8", features = ["rt", "sync", "time"] }
+toml = "0.5"
+tracing = { version = "0.1", features = ["attributes", "log"] }
+
+[dev-dependencies]
+tempfile = "3.3"
+test-case = "1.2"
diff --git a/crates/core/c8y_api/src/error.rs b/crates/core/c8y_api/src/error.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/crates/core/c8y_api/src/error.rs
@@ -0,0 +1 @@
+
diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs
new file mode 100644
index 00000000..2a56946a
--- /dev/null
+++ b/crates/core/c8y_api/src/http_proxy.rs
@@ -0,0 +1,421 @@
+use crate::json_c8y::{
+ C8yCreateEvent, C8yManagedObject, C8yUpdateSoftwareListResponse, InternalIdResponse,
+};
+use async_trait::async_trait;
+use c8y_smartrest::{
+ error::SMCumulocityMapperError, smartrest_deserializer::SmartRestJwtResponse, topic::C8yTopic,
+};
+use mqtt_channel::{Connection, PubChannel, StreamExt, Topic, TopicFilter};
+use reqwest::Url;
+use std::time::Duration;
+use tedge_config::{
+ get_tedge_config, C8yUrlSetting, ConfigSettingAccessor, ConfigSettingAccessorStringExt,
+ DeviceIdSetting, MqttPortSetting, TEdgeConfig,
+};
+use time::{format_description, OffsetDateTime};
+
+use serde::{Deserialize, Serialize};
+use tracing::{error, info, instrument};
+
+const RETRY_TIMEOUT_SECS: u64 = 60;
+
+/// creates an mqtt client with a given `session_name`
+pub async fn create_mqtt_client(
+ session_name: &str,
+) -> Result<mqtt_channel::Connection, SMCumulocityMapperError> {
+ let tedge_config = get_tedge_config()?;
+ let mqtt_port = tedge_config.query(MqttPortSetting)?.into();
+ let mqtt_config = mqtt_channel::Config::default()
+ .with_port(mqtt_port)
+ .with_session_name(session_name)
+ .with_subscriptions(mqtt_channel::TopicFilter::new_unchecked(
+ C8yTopic::SmartRestResponse.as_str(),
+ ));
+
+ let mqtt_client = mqtt_channel::Connection::new(&mqtt_config).await?;
+ Ok(mqtt_client)
+}
+
+/// creates an http client with a given `session_name`
+pub async fn create_http_client(
+ session_name: &str,
+) -> Result<JwtAuthHttpProxy, SMCumulocityMapperError> {
+ let config = get_tedge_config()?;
+ let mut http_proxy = JwtAuthHttpProxy::try_new(&config, &session_name).await?;
+ let () = http_proxy.init().await?;
+ Ok(http_proxy)
+}
+
+/// An HttpProxy handles http requests to C8y on behalf of the device.
+#[async_trait]
+pub trait C8YHttpProxy {
+ async fn init(&mut self) -> Result<(), SMCumulocityMapperError>;
+
+ fn url_is_in_my_tenant_domain(&self, url: &str) -> bool;
+
+ async fn get_jwt_token(&mut self) -> Result<SmartRestJwtResponse, SMCumulocityMapperError>;
+
+ async fn send_software_list_http(
+ &mut self,
+ c8y_software_list: &C8yUpdateSoftwareListResponse,
+ ) -> Result<(), SMCumulocityMapperError>;
+
+ async fn upload_log_binary(
+ &mut self,
+ log_content: &str,
+ ) -> Result<String, SMCumulocityMapperError>;
+}
+
+/// Define a C8y endpoint
+pub struct C8yEndPoint {
+ c8y_host: String,
+ device_id: String,
+ c8y_internal_id: String,
+}
+
+impl C8yEndPoint {
+ #[cfg(test)]
+ fn new(c8y_host: &str, device_id: &str, c8y_internal_id: &str) -> C8yEndPoint {
+ C8yEndPoint {
+ c8y_host: c8y_host.into(),
+ device_id: device_id.into(),
+ c8y_internal_id: c8y_internal_id.into(),
+ }
+ }
+
+ fn get_url_for_sw_list(&self) -> String {
+ let mut url_update_swlist = String::new();
+ url_update_swlist.push_str("https://");
+ url_update_swlist.push_str(&self.c8y_host);
+ url_update_swlist.push_str("/inventory/managedObjects/");
+ url_update_swlist.push_str(&self.c8y_internal_id);
+
+ url_update_swlist
+ }
+
+ fn get_url_for_get_id(&self) -> String {
+ let mut url_get_id = String::new();
+ url_get_id.push_str("https://");
+ url_get_id.push_str(&self.c8y_host);
+ url_get_id.push_str("/identity/externalIds/c8y_Serial/");
+ url_get_id.push_str(&self.device_id);
+
+ url_get_id
+ }
+
+ fn get_url_for_create_event(&self) -> String {
+ let mut url_create_event = String::new();
+ url_create_event.push_str("https://");
+ url_create_event.push_str(&self.c8y_host);
+ url_create_event.push_str("/event/events/");
+
+ url_create_event
+ }
+
+ fn get_url_for_event_binary_upload(&self, event_id: &str) -> String {
+ let mut url_event_binary = self.get_url_for_create_event();
+ url_event_binary.push_str(event_id);
+ url_event_binary.push_str("/binaries");
+
+ url_event_binary
+ }
+
+ fn url_is_in_my_tenant_domain(&self, url: &str) -> bool {
+ // c8y URL may contain either `Tenant Name` or Tenant Id` so they can be one of following options:
+ // * <tenant_name>.<domain> eg: sample.c8y.io
+ // * <tenant_id>.<domain> eg: t12345.c8y.io
+ // These URLs may be both equivalent and point to the same tenant.
+ // We are going to remove that and only check if the domain is the same.
+ let tenant_uri = &self.c8y_host;
+ let url_host = match Url::parse(url) {
+ Ok(url) => match url.host() {
+ Some(host) => host.to_string(),
+ None => return false,
+ },
+ Err(_err) => {
+ return false;
+ }
+ };
+
+ let url_domain = url_host.splitn(2, '.').collect::<Vec<&str>>();
+ let tenant_domain = tenant_uri.splitn(2, '.').collect::<Vec<&str>>();
+ if url_domain.get(1) == tenant_domain.get(1) {
+ return true;
+ }
+ false
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+/// used to retrieve the id of a log event
+pub struct SmartRestLogEvent {
+ pub id: String,
+}
+
+/// An HttpProxy that uses MQTT to retrieve JWT tokens and authenticate the device
+///
+/// - Keep the connection info to c8y and the internal Id of the device
+/// - Handle JWT requests
+pub struct JwtAuthHttpProxy {
+ mqtt_con: mqtt_channel::Connection,
+ http_con: reqwest::Client,
+ end_point: C8yEndPoint,
+}
+
+impl JwtAuthHttpProxy {
+ pub fn new(
+ mqtt_con: mqtt_channel::Connection,
+ http_con: reqwest::Client,
+ c8y_host: &str,
+ device_id: &str,
+ ) -> JwtAuthHttpProxy {
+ JwtAuthHttpProxy {
+ mqtt_con,
+ http_con,
+ end_point: C8yEndPoint {
+ c8y_host: c8y_host.into(),
+ device_id: device_id.into(),
+ c8y_internal_id: "".into(),
+ },
+ }
+ }
+
+ pub async fn try_new(
+ tedge_config: &TEdgeConfig,
+ session_name: &str,
+ ) -> Result<JwtAuthHttpProxy, SMCumulocityMapperError> {
+ let c8y_host = tedge_config.query_string(C8yUrlSetting)?;
+ let device_id = tedge_config.query_string(DeviceIdSetting)?;
+ let http_con = reqwest::ClientBuilder::new().build()?;
+
+ let mqtt_port = tedge_config.query(MqttPortSetting)?.into();
+ let topic = TopicFilter::new("c8y/s/dat")?;
+ let mqtt_config = mqtt_channel::Config::default()
+ .with_port(mqtt_port)
+ .with_clean_session(true)
+ .with_session_name(session_name)
+ .with_subscriptions(topic);
+ let mut mqtt_con = Connection::new(&mqtt_config).await?;
+
+ // Ignore errors on this connection
+ let () = mqtt_con.errors.close();
+
+ Ok(JwtAuthHttpProxy::new(
+ mqtt_con, http_con, &c8y_host, &device_id,
+ ))
+ }
+
+ async fn try_get_and_set_internal_id(&mut self) -> Result<(), SMCumulocityMapperError> {
+ let token = self.get_jwt_token().await?;
+ let url_get_id = self.end_point.get_url_for_get_id();
+
+ self.end_point.c8y_internal_id = self
+ .try_get_internal_id(&url_get_id, &token.token())
+ .await?;
+
+ Ok(())
+ }
+
+ async fn try_get_internal_id(
+ &self,
+ url_get_id: &str,
+ token: &str,
+ ) -> Result<String, SMCumulocityMapperError> {
+ let internal_id = self
+ .http_con
+ .get(url_get_id)
+ .bearer_auth(token)
+ .send()
+ .await?;
+ let internal_id_response = internal_id.json::<InternalIdResponse>().await?;
+
+ let internal_id = internal_id_response.id();
+ Ok(internal_id)
+ }
+
+ /// Make a POST request to /event/events and return the event id from response body.
+ /// The event id is used to upload the binary.
+ fn create_log_event(&self) -> C8yCreateEvent {
+ let local = OffsetDateTime::now_utc();
+
+ let c8y_managed_object = C8yManagedObject {
+ id: self.end_point.c8y_internal_id.clone(),
+ };
+
+ C8yCreateEvent::new(
+ c8y_managed_object,
+ "c8y_Logfile",
+ &local
+ .format(&format_description::well_known::Rfc3339)
+ .unwrap(),
+ "software-management",
+ )
+ }
+
+ async fn get_event_id(
+ &mut self,
+ c8y_event: C8yCreateEvent,
+ ) -> Result<String, SMCumulocityMapperError> {
+ let token = self.get_jwt_token().await?;
+ let create_event_url = self.end_point.get_url_for_create_event();
+
+ let request = self
+ .http_con
+ .post(create_event_url)
+ .json(&c8y_event)
+ .bearer_auth(token.token())
+ .header("Accept", "application/json")
+ .timeout(Duration::from_millis(10000))
+ .build()?;
+
+ let response = self.http_con.execute(request).await?;
+ dbg!(&response);
+ let event_response_body = response.json::<SmartRestLogEvent>().await?;
+
+ Ok(event_response_body.id)
+ }
+}
+
+#[async_trait]
+impl C8YHttpProxy for JwtAuthHttpProxy {
+ fn url_is_in_my_tenant_domain(&self, url: &str) -> bool {
+ self.end_point.url_is_in_my_tenant_domain(url)
+ }
+
+ #[instrument(skip(self), name = "init")]
+ async fn init(&mut self) -> Result<(), SMCumulocityMapperError> {
+ info!("Initialisation");
+ while self.end_point.c8y_internal_id.is_empty() {
+ if let Err(error) = self.try_get_and_set_internal_id().await {
+ error!(
+ "An error ocurred while retrieving internal Id, operation will retry in {} seconds and mapper will reinitialise.\n Error: {:?}",
+ RETRY_TIMEOUT_SECS, error
+ );
+
+ tokio::time::sleep(Duration::from_secs(RETRY_TIMEOUT_SECS)).await;
+ continue;
+ };
+ }
+ info!("Initialisation done.");
+ Ok(())
+ }
+
+ async fn get_jwt_token(&mut self) -> Result<SmartRestJwtResponse, SMCumulocityMapperError> {
+ let () = self
+ .mqtt_con
+ .published
+ .publish(mqtt_channel::Message::new(
+ &Topic::new_unchecked("c8y/s/uat"),
+ "".to_string(),
+ ))
+ .await?;
+ let token_smartrest = match tokio::time::timeout(
+ Duration::from_secs(10),
+ self.mqtt_con.received.next(),
+ )
+ .await
+ {
+ Ok(Some(msg)) => msg.payload_str()?.to_string(),
+ Ok(None) => return Err(SMCumulocityMapperError::InvalidMqttMessage),
+ Err(_elapsed) => return Err(SMCumulocityMapperError::RequestTimeout),
+ };
+
+ Ok(SmartRestJwtResponse::try_new(&token_smartrest)?)
+ }
+
+ async fn send_software_list_http(
+ &mut self,
+ c8y_software_list: &C8yUpdateSoftwareListResponse,
+ ) -> Result<(), SMCumulocityMapperError> {
+ let url = self.end_point.get_url_for_sw_list();
+ let token = self.get_jwt_token().await?;
+
+ let request = self
+ .http_con
+ .put(url)
+ .json(c8y_software_list)
+ .bearer_auth(&token.token())
+ .timeout(Duration::from_millis(10000))
+ .build()?;
+
+ let _response = self.http_con.execute(request).await?;
+
+ Ok(())
+ }
+
+ async fn upload_log_binary(
+ &mut self,
+ log_content: &str,
+ ) -> Result<String, SMCumulocityMapperError> {
+ let token = self.get_jwt_token().await?;
+
+ let log_event = self.create_log_event();
+ let event_response_id = self.get_event_id(log_event).await?;
+ let binary_upload_event_url = self
+ .end_point
+ .get_url_for_event_binary_upload(&event_response_id);
+
+ let request = self
+ .http_con
+ .post(&binary_upload_event_url)
+ .header("Accept", "application/json")
+ .header("Content-Type", "text/plain")
+ .body(log_content.to_string())
+ .bearer_auth(token.token())
+ .timeout(Duration::from_millis(10000))
+ .build()?;
+
+ let _response = self.http_con.execute(request).await?;
+ Ok(binary_upload_event_url)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use test_case::test_case;
+
+ #[test]
+ fn get_url_for_get_id_returns_correct_address() {
+ let c8y = C8yEndPoint::new("test_host", "test_device", "internal-id");
+ let res = c8y.get_url_for_get_id();
+
+ assert_eq!(
+ res,
+ "https://test_host/identity/externalIds/c8y_Serial/test_device"
+ );
+ }
+
+ #[test]
+ fn get_url_for_sw_list_returns_correct_address() {
+ let c8y = C8yEndPoint::new("test_host", "test_device", "12345");
+ let res = c8y.get_url_for_sw_list();
+
+ assert_eq!(res, "https://test_host/inventory/managedObjects/12345");
+ }
+
+ #[test_case("http://aaa.test.com")]
+ #[test_case("https://aaa.test.com")]
+ #[test_case("ftp://aaa.test.com")]
+ #[test_case("mqtt://aaa.test.com")]
+ #[test_case("https://t1124124.test.com")]
+ #[test_case("https://t1124124.test.com:12345")]
+ #[test_case("https://t1124124.test.com/path")]
+ #[test_case("https://t1124124.test.com/path/to/file.test")]
+ #[test_case("https://t1124124.test.com/path/to/file")]
+ fn url_is_my_tenant_correct_urls(url: &str) {
+ let c8y = C8yEndPoint::new("test.test.com", "test_device", "internal-id");
+ assert!(c8y.url_is_in_my_tenant_domain(url));
+ }
+
+ #[test_case("test.com")]
+ #[test_case("http://test.co")]
+ #[test_case("http://test.co.te")]
+ #[test_case("http://test.com:123456")]
+ #[test_case("http://test.com::12345")]
+ fn url_is_my_tenant_incorrect_urls(url: &str) {
+ let c8y = C8yEndPoint::new("test.test.com", "test_device", "internal-id");
+ assert!(!c8y.url_is_in_my_tenant_domain(url));
+ }
+}
diff --git a/crates/core/c8y_api/src/json_c8y.rs b/crates/core/c8y_api/src/json_c8y.rs
new file mode 100644
index 00000000..8566a84c
--- /dev/null
+++ b/crates/core/c8y_api/src/json_c8y.rs
@@ -0,0 +1,282 @@
+use agent_interface::{
+ Jsonify, SoftwareListResponse, SoftwareModule, SoftwareType, SoftwareVersion,
+};
+
+use download::DownloadInfo;
+use serde::{Deserialize, Serialize};
+
+const EMPTY_STRING: &str = "";
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct C8yCreateEvent {
+ source: C8yManagedObject,
+ #[serde(rename = "type")]
+ event_type: String,
+ time: String,
+ text: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct C8yManagedObject {
+ pub id: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InternalIdResponse {
+ managed_object: C8yManagedObject,
+ external_id: String,
+}
+
+impl InternalIdResponse {
+ pub fn id(&self) -> String {
+ self.managed_object.id.clone()
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct C8ySoftwareModuleItem {
+ pub name: String,
+ pub version: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(flatten)]
+ pub url: Option<DownloadInfo>,
+}
+
+impl<'a> Jsonify<'a> for C8ySoftwareModuleItem {}
+
+impl From<SoftwareModule> for C8ySoftwareModuleItem {
+ fn from(module: SoftwareModule) -> Self {
+ let url = if module.url.is_none() {
+ Some(EMPTY_STRING.into())
+ } else {
+ module.url
+ };
+
+ Self {
+ name: module.name,
+ version: Option::from(combine_version_and_type(
+ &module.version,
+ &module.module_type,
+ )),
+ url,
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct C8yUpdateSoftwareListResponse {
+ #[serde(rename = "c8y_SoftwareList")]
+ c8y_software_list: Option<Vec<C8ySoftwareModuleItem>>,
+}
+
+impl<'a> Jsonify<'a> for C8yUpdateSoftwareListResponse {}
+
+impl From<&SoftwareListResponse> for C8yUpdateSoftwareListResponse {
+ fn from(list: &SoftwareListResponse) -> Self {
+ let mut new_list: Vec<C8ySoftwareModuleItem> = Vec::new();
+ list.modules().into_iter().for_each(|software_module| {
+ let c8y_software_module: C8ySoftwareModuleItem = software_module.into();
+ new_list.push(c8y_software_module);
+ });
+
+ Self {
+ c8y_software_list: Some(new_list),
+ }
+ }
+}
+
+impl C8yCreateEvent {
+ pub fn new(source: C8yManagedObject, event_type: &str, time: &str, text: &str) -> Self {
+ Self {
+ source,
+ event_type: event_type.into(),
+ time: time.into(),
+ text: text.into(),
+ }
+ }
+}
+
+impl<'a> Jsonify<'a> for C8yCreateEvent {}
+
+fn combine_version_and_type(
+ version: &Option<SoftwareVersion>,
+ module_type: &Option<SoftwareType>,
+) -> String {
+ match module_type {
+ Some(m) => {
+ if m.is_empty() {
+ match version {
+ Some(v) => v.into(),
+ None => EMPTY_STRING.into(),
+ }
+ } else {
+ match version {
+ Some(v) => format!("{}::{}", v, m),
+ None => format!("::{}", m),
+ }
+ }
+ }
+ None => match version {
+ Some(v) => {
+ if v.contains("::") {
+ format!("{}::", v)
+ } else {
+ v.into()
+ }
+ }
+ None => EMPTY_STRING.into(),
+ },
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn from_software_module_to_c8y_software_module_item() {
+ let software_module = SoftwareModule {
+ module_type: Some("a".into()),
+ name: "b".into(),
+ version: Some("c".into()),
+ url: Some("".into()),
+ file_path: None,
+ };
+
+ let expected_c8y_item = C8ySoftwareModuleItem {
+ name: "b".into(),
+ version: Some("c::a".into()),
+ url: Some("".into()),
+ };
+
+ let converted: C8ySoftwareModuleItem = software_module.into();
+
+ assert_eq!(converted, expected_c8y_item);
+ }
+
+ #[test]
+ fn from_thin_edge_json_to_c8y_set_software_list() {
+ let input_json = r#"{
+ "id":"1",
+ "status":"successful",
+ "currentSoftwareList":[
+ {"type":"debian", "modules":[
+ {"name":"a"},
+ {"name":"b","version":"1.0"},
+ {"name":"c","url":"https://foobar.io/c.deb"},
+ {"name":"d","version":"beta","url":"https://foobar.io/d.deb"}
+ ]},
+ {"type":"apama","modules":[
+ {"name":"m","url":"https://foobar.io/m.epl"}
+ ]}
+ ]}"#;
+
+ let json_obj = &SoftwareListResponse::from_json(input_json).unwrap();
+
+ let c8y_software_list: C8yUpdateSoftwareListResponse = json_obj.into();
+
+ let expected_struct = C8yUpdateSoftwareListResponse {
+ c8y_software_list: Some(vec![
+ C8ySoftwareModuleItem {
+ name: "a".into(),
+ version: Some("::debian".into()),
+ url: Some("".into()),
+ },
+ C8ySoftwareModuleItem {
+ name: "b".into(),
+ version: Some("1.0::debian".into()),
+ url: Some("".into()),
+ },
+ C8ySoftwareModuleItem {
+ name: "c".into(),
+ version: Some("::debian".into()),
+ url: Some("https://foobar.io/c.deb".into()),
+ },
+ C8ySoftwareModuleItem {
+ name: "d".into(),
+ version: Some("beta::debian".into()),
+ url: Some("https://foobar.io/d.deb".into()),
+ },
+ C8ySoftwareModuleItem {
+ name: "m".into(),
+ version: Some("::apama".into()),
+ url: Some("https://foobar.io/m.epl".into()),
+ },
+ ]),
+ };
+
+ let expected_json = r#"{"c8y_SoftwareList":[{"name":"a","version":"::debian","url":""},{"name":"b","version":"1.0::debian","url":""},{"name":"c","version":"::debian","url":"https://foobar.io/c.deb"},{"name":"d","version":"beta::debian","url":"https://foobar.io/d.deb"},{"name":"m","version":"::apama","url":"https://foobar.io/m.epl"}]}"#;
+
+ assert_eq!(c8y_software_list, expected_struct);
+ assert_eq!(c8y_software_list.to_json().unwrap(), expected_json);
+ }
+
+ #[test]
+ fn empty_to_c8y_set_software_list() {
+ let input_json = r#"{
+ "id":"1",
+ "status":"successful",
+ "currentSoftwareList":[]
+ }"#;
+
+ let json_obj = &SoftwareListResponse::from_json(input_json).unwrap();
+ let c8y_software_list: C8yUpdateSoftwareListResponse = json_obj.into();
+
+ let expected_struct = C8yUpdateSoftwareListResponse {
+ c8y_software_list: Some(vec![]),
+ };
+ let expected_json = r#"{"c8y_SoftwareList":[]}"#;
+
+ assert_eq!(c8y_software_list, expected_struct);
+ assert_eq!(c8y_software_list.to_json().unwrap(), expected_json);
+ }
+
+ #[test]
+ fn get_id_from_c8y_response() {
+ let managed_object = C8yManagedObject { id: "12345".into() };
+ let response = InternalIdResponse {
+ managed_object,
+ external_id: "test".into(),
+ };
+
+ assert_eq!(response.id(), "12345".to_string());
+ }
+
+ #[test]
+ fn verify_combine_version_and_type() {
+ let some_version: Option<SoftwareVersion> = Some("1.0".to_string());
+ let some_version_with_colon: Option<SoftwareVersion> = Some("1.0.0::1".to_string());
+ let none_version: Option<SoftwareVersion> = None;
+ let some_module_type: Option<SoftwareType> = Some("debian".to_string());
+ let none_module_type: Option<SoftwareType> = None;
+
+ assert_eq!(
+ combine_version_and_type(&some_version, &some_module_type),
+ "1.0::debian"
+ );
+ assert_eq!(
+ combine_version_and_type(&some_version, &none_module_type),
+ "1.0"
+ );
+ assert_eq!(
+ combine_version_and_type(&some_version_with_colon, &some_module_type),
+ "1.0.0::1::debian"
+ );
+ assert_eq!(
+ combine_version_and_type(&some_version_with_colon, &none_module_type),
+ "1.0.0::1::"
+ );
+ assert_eq!(
+ combine_version_and_type(&none_version, &some_module_type),
+ "::debian"
+ );
+ assert_eq!(
+ combine_version_and_type(&none_version, &none_module_type),
+ EMPTY_STRING
+ );
+ }
+}
diff --git a/crates/core/c8y_api/src/lib.rs b/crates/core/c8y_api/src/lib.rs
new file mode 100644
index 00000000..2a0286e0
--- /dev/null
+++ b/crates/core/c8y_api/src/lib.rs
@@ -0,0 +1,12 @@
+pub mod error;
+pub mod http_proxy;
+pub mod json_c8y;
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn it_works() {
+ let result = 2 + 2;
+ assert_eq!(result, 4);
+ }
+}