Skip to content

Commit

Permalink
Extract utils from main function and unit test it
Browse files Browse the repository at this point in the history
Related-to: TOR-3521

Signed-off-by: Leonardo Held <[email protected]>
  • Loading branch information
leonheldattoradex committed Aug 20, 2024
1 parent d44e167 commit 1ef9c32
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 75 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ webpki-roots = "0.26.3"
x509-parser = "0.16.0"
zbus = "4.3.1"

[dev-dependencies]
tempfile = "3.4"

[features]
test_mode = []
81 changes: 6 additions & 75 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,94 +7,25 @@
#![warn(clippy::print_stdout)]
#![warn(clippy::print_stderr)]

use std::{io::ErrorKind, path::Path, time::Duration};
use std::{io::ErrorKind, time::Duration};

use dbus::ServiceEvent;
use log::*;

use eyre::{Context, OptionExt};
use eyre::Context;
use rumqttc::{
tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer},
ConnectionError,
};
use tokio::sync::mpsc::Sender;

mod dbus;
mod utils;
mod utils_test;

type Result<T> = std::result::Result<T, eyre::Report>;

// blocking
fn load_cert<P: AsRef<Path>>(filename: P) -> Result<CertificateDer<'static>> {
let certfile =
std::fs::File::open(&filename).context(format!("opening {:?}", filename.as_ref()))?;
let mut reader = std::io::BufReader::new(&certfile);
let mut all_certs: Vec<CertificateDer> = rustls_pemfile::certs(&mut reader).flatten().collect();

if all_certs.len() != 1 {
eyre::bail!(
"invalid number of certificates in {:?}, expected 1 got {:?}",
filename.as_ref(),
all_certs.len()
);
}

#[allow(clippy::unwrap_used)] // length checked
Ok(all_certs.pop().unwrap())
}

// blocking
fn load_private_key<P: AsRef<Path>>(filename: P) -> Result<PrivateKeyDer<'static>> {
let keyfile =
std::fs::File::open(&filename).context(format!("read {:?}", filename.as_ref()))?;
let mut reader = std::io::BufReader::new(keyfile);
use utils::{load_cert, load_private_key, read_device_id, parse_payload};

loop {
match rustls_pemfile::read_one(&mut reader)? {
Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()),
Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()),
Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()),
None => break,
_ => {}
}
}

eyre::bail!(
"no keys found in {:?} (encrypted keys not supported)",
filename.as_ref()
);
}

fn read_device_id(cert: &CertificateDer) -> Result<String> {
use x509_parser::prelude::*;

let (_, res) = X509Certificate::from_der(cert)?;

let common_name = res
.subject
.iter_common_name()
.flat_map(|c| c.as_str())
.next();

Ok(common_name
.ok_or(eyre::eyre!("Could not extract uuid from certificate CN"))?
.to_owned())
}

fn parse_payload(payload: &[u8]) -> Result<(String, serde_json::Value)> {
let mut payload_json: serde_json::Value = serde_json::from_slice(payload)?;

let args_json = payload_json
.pointer_mut("/args")
.ok_or_eyre("message payload did not include `args`")?
.take();

let command = payload_json
.pointer("/command")
.and_then(|s| s.as_str().map(|s| s.to_owned()))
.ok_or_eyre("message payload did not include `command`")?;

Ok((command, args_json))
}
type Result<T> = std::result::Result<T, eyre::Report>;

async fn run() -> Result<()> {
let mqtt_hostname = std::env::var("TZN_MQTT_HOST").unwrap_or("mqtt.dev.torizon.io".to_owned());
Expand Down
10 changes: 10 additions & 0 deletions src/test_data/client.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBdTCCARygAwIBAgIUXTaOb4/pIH1jZ+RyhAZvdi41/0QwCgYIKoZIzj0EAwIw
GTEXMBUGA1UEAwwOb3RhLWRldmljZXMtQ0EwIBcNMjQwODE5MTQxMzQ3WhgPMjEy
NDA4MTkxNDEzNDdaMC8xLTArBgNVBAMTJDg4OGIyNTQ1LWEzMGYtNDg5Ni1hMzc3
LWI0N2M0YjcyNTM3NDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMQh0ZB30O5x
Pp7k5vICJ0QOdxoubO0VtTCJX+zQ2oCmjFBatfUH5gjuNsdmCw1zf7FeQCDyYnP9
OQRoHLPBcwyjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF
BQcDAjAKBggqhkjOPQQDAgNHADBEAiBmj6SenB1MEca664lGUkDIdsPzYAQ7G1Ml
tipTcMGIpwIgPbJMmfRXiSZjLGjdmhr43cG8S3ALrcXIHDyQdVmA8lI=
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions src/test_data/client.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIM5yZsvOEcDQY571vHH6UJQwKPjvyoYgcgwKF4pwu6tgoAoGCCqGSM49
AwEHoUQDQgAExCHRkHfQ7nE+nuTm8gInRA53Gi5s7RW1MIlf7NDagKaMUFq19Qfm
CO42x2YLDXN/sV5AIPJic/05BGgcs8FzDA==
-----END EC PRIVATE KEY-----
83 changes: 83 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2024 Toradex A.G.
// SPDX-License-Identifier: Apache-2.0

use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use rustls_pemfile;
use eyre::{Context, Result, OptionExt};
use x509_parser::prelude::*;
use serde_json;
use crate::CertificateDer;
use crate::PrivateKeyDer;

// blocking
pub fn load_cert<P: AsRef<Path>>(filename: P) -> Result<CertificateDer<'static>> {
let certfile =
File::open(&filename).context(format!("opening {:?}", filename.as_ref()))?;
let mut reader = BufReader::new(&certfile);
let mut all_certs: Vec<CertificateDer> = rustls_pemfile::certs(&mut reader).flatten().collect();

if all_certs.len() != 1 {
eyre::bail!(
"invalid number of certificates in {:?}, expected 1 got {:?}",
filename.as_ref(),
all_certs.len()
);
}

#[allow(clippy::unwrap_used)] // length checked
Ok(all_certs.pop().unwrap())
}

// blocking
pub fn load_private_key<P: AsRef<Path>>(filename: P) -> Result<PrivateKeyDer<'static>> {
let keyfile =
File::open(&filename).context(format!("read {:?}", filename.as_ref()))?;
let mut reader = BufReader::new(keyfile);

loop {
match rustls_pemfile::read_one(&mut reader)? {
Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()),
Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()),
Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()),
None => break,
_ => {}
}
}

eyre::bail!(
"no keys found in {:?} (encrypted keys not supported)",
filename.as_ref()
);
}

pub fn read_device_id(cert: &CertificateDer) -> Result<String> {
let (_, res) = X509Certificate::from_der(cert)?;

let common_name = res
.subject
.iter_common_name()
.flat_map(|c| c.as_str())
.next();

Ok(common_name
.ok_or(eyre::eyre!("Could not extract uuid from certificate CN"))?
.to_owned())
}

pub fn parse_payload(payload: &[u8]) -> Result<(String, serde_json::Value)> {
let mut payload_json: serde_json::Value = serde_json::from_slice(payload)?;

let args_json = payload_json
.pointer_mut("/args")
.ok_or_eyre("message payload did not include `args`")?
.take();

let command = payload_json
.pointer("/command")
.and_then(|s| s.as_str().map(|s| s.to_owned()))
.ok_or_eyre("message payload did not include `command`")?;

Ok((command, args_json))
}
65 changes: 65 additions & 0 deletions src/utils_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2024 Toradex A.G.
// SPDX-License-Identifier: Apache-2.0

use std::path::{Path};

use crate::utils::{load_cert, load_private_key, read_device_id, parse_payload};

#[cfg(test)]
mod tests {
use super::*;


#[test]
fn test_load_cert() {
let cert_path = Path::new("src/test_data/client.crt");
let result = load_cert(cert_path);
assert!(result.is_ok(), "Failed to load certificate: {:?}", result.err());

let cert = result.expect("Valid certificate");
assert!(cert.len() > 0, "Certificate data should not be empty");
}

#[test]
fn test_load_private_key() {
let key_path = Path::new("src/test_data/client.key");
let result = load_private_key(key_path);
assert!(result.is_ok(), "Failed to load private key: {:?}", result.err());
}

#[test]
fn test_read_device_id() {
let cert_path = Path::new("src/test_data/client.crt");
let cert = load_cert(cert_path).expect("Failed to load cert");
let result = read_device_id(&cert);
assert!(result.is_ok(), "Failed to read device id: {:?}", result.err());

let device_id = result.expect("Valid device ID");
assert!(!device_id.is_empty(), "Device ID should not be empty");
}

#[test]
fn test_parse_payload() {
let payload = br#"{"command": "test", "args": {"key": "value"}}"#;
let result = parse_payload(payload);
assert!(result.is_ok(), "Failed to parse payload: {:?}", result.err());

let (command, args) = result.expect("Valid command and args");
assert_eq!(command, "test");
assert_eq!(args["key"], "value");
}

#[test]
fn test_parse_payload_missing_args() {
let payload = br#"{"command": "test"}"#;
let result = parse_payload(payload);
assert!(result.is_err(), "Expected error for missing `args`, got: {:?}", result);
}

#[test]
fn test_parse_payload_missing_command() {
let payload = br#"{"args": {"key": "value"}}"#;
let result = parse_payload(payload);
assert!(result.is_err(), "Expected error for missing `command`, got: {:?}", result);
}
}

0 comments on commit 1ef9c32

Please sign in to comment.