From 172f8163139f8112b76d462198a1213a5cb49dde Mon Sep 17 00:00:00 2001 From: Gus Power Date: Sat, 7 Jun 2025 14:55:21 +0100 Subject: first test of Junie - add network module for detecting available network adapters --- src/error.rs | 12 ++++++-- src/http.rs | 46 +++++++++++++++++++++++------ src/lib.rs | 11 +++++++ src/main.rs | 3 +- src/network.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/network.rs (limited to 'src') diff --git a/src/error.rs b/src/error.rs index 1ce9482..1b71511 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ use homedir::GetHomeError; +use network_interface::Error as NetworkInterfaceError; use reqwest::{Error as ReqwestError, Url}; use serde_json::Error as JsonError; use std::fmt; @@ -16,6 +17,7 @@ pub enum AppError { InvalidResponse { url: Url, reason: String }, InvalidHttpHeader(String), UnableToGetHomeDirectory(GetHomeError), + NetworkInterfaceError(NetworkInterfaceError), } impl fmt::Display for AppError { @@ -34,11 +36,16 @@ impl fmt::Display for AppError { Self::RequestFailed { url, .. } => write!(f, "Request to {} failed", url), Self::InvalidResponse { url, reason } => { write!(f, "Invalid response from {}: {}", url, reason) - }, - Self::InvalidHttpHeader(message) => write!(f, "Invalid HTTP header configuration: {}", message), + } + Self::InvalidHttpHeader(message) => { + write!(f, "Invalid HTTP header configuration: {}", message) + } Self::UnableToGetHomeDirectory(err) => { write!(f, "Failed to get home directory: {}", err) } + Self::NetworkInterfaceError(err) => { + write!(f, "Network interface error: {}", err) + } } } } @@ -49,6 +56,7 @@ impl std::error::Error for AppError { Self::IoError(err) => Some(err), Self::ConfigParseError { source, .. } => Some(source), Self::RequestFailed { source, .. } => Some(source), + Self::NetworkInterfaceError(err) => Some(err), _ => None, } } diff --git a/src/http.rs b/src/http.rs index db5e3a6..4ea9b96 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,9 +1,9 @@ -use serde_with::DisplayFromStr; +use crate::error::{AppError, AppResult}; use http::{HeaderMap, HeaderName, HeaderValue, Method}; use reqwest::{Client, Request, Url}; use serde::{Deserialize, Serialize}; +use serde_with::DisplayFromStr; use serde_with::serde_as; -use crate::error::{AppError, AppResult}; #[serde_as] #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -18,9 +18,12 @@ pub struct RequestConfig { } impl RequestConfig { - pub fn new(method: Method, url: Url, headers: Vec<(String, String)>) -> Self { - Self { method, url, headers } + Self { + method, + url, + headers, + } } pub fn get(url: Url, headers: Vec<(String, String)>) -> Self { @@ -32,6 +35,8 @@ impl RequestConfig { if !self.headers.is_empty() { builder = builder.headers(self.header_map()?); } + builder = builder.body("OHAI"); + Ok(builder.build()?) } @@ -43,25 +48,30 @@ impl RequestConfig { let mut header_map = HeaderMap::new(); for (name, value) in &self.headers { let name: HeaderName = name.try_into().map_err(|e| { - AppError::InvalidHttpHeader(format!("[{}] is not a valid HTTP header name ({})", name, e)) + AppError::InvalidHttpHeader(format!( + "[{}] is not a valid HTTP header name ({})", + name, e + )) })?; let value: HeaderValue = value.parse().map_err(|e| { - AppError::InvalidHttpHeader(format!("[{}] is not a valid HTTP header value ({})", value, e)) + AppError::InvalidHttpHeader(format!( + "[{}] is not a valid HTTP header value ({})", + value, e + )) })?; header_map.insert(name, value); } Ok(header_map) } - } #[cfg(test)] mod tests { + use super::*; use crate::AppError; use crate::{assert_error, assert_invalid_http_header, test}; use reqwest::ClientBuilder; - use super::*; test! { fn simple_get_request() { @@ -72,6 +82,25 @@ mod tests { } } + test! { + fn get_with_arbitrary_header() { + let url = Url::parse("https://example.com")?; + let actual = build_get_request(&url, vec![("X-Arbitrary".to_string(), "X-Arbitrary-Value".to_string())])?; + assert_eq!(actual.headers().get("X-Arbitrary").unwrap().to_str()?, "X-Arbitrary-Value"); + } + } + + test! { + fn post_request_with_text_body() { + let url = Url::parse("https://example.com")?; + let config = RequestConfig::new(Method::POST, url, vec![]); + let client = ClientBuilder::new().build()?; + let request = config.build(&client)?; + + assert_eq!(request.method(), Method::POST); + } + } + test! { fn invalid_header_name() { let url = Url::parse("https://example.com")?; @@ -93,5 +122,4 @@ mod tests { let client = ClientBuilder::new().build()?; config.build(&client) } - } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..083cf46 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +mod config; +mod dyndns_service; +mod error; +mod http; +mod ip_service; + +#[cfg(test)] +#[macro_use] +pub mod test_macros; + +pub use error::{AppError, AppResult}; diff --git a/src/main.rs b/src/main.rs index 2843eb3..3db2582 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,10 +12,11 @@ mod config; mod dyndns_service; mod error; mod ip_service; +mod network; +mod http; #[cfg(test)] mod test_macros; -mod http; #[derive(Parser, Debug)] #[command(ignore_errors(true), version, about, long_about = None)] diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..3697e29 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,93 @@ +use crate::error::{AppError, AppResult}; +use network_interface::{NetworkInterface, NetworkInterfaceConfig}; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkAdapter { + pub name: String, + pub is_up: bool, + pub is_loopback: bool, +} + +impl fmt::Display for NetworkAdapter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl From for NetworkAdapter { + fn from(interface: NetworkInterface) -> Self { + let is_loopback = interface.name.to_lowercase().contains("lo"); + let is_up = !interface.addr.is_empty(); + + Self { + name: interface.name, + is_up, + is_loopback, + } + } +} + +pub fn get_network_adapters() -> AppResult> { + NetworkInterface::show() + .map_err(AppError::NetworkInterfaceError) + .map(|interfaces| interfaces.into_iter().map(NetworkAdapter::from).collect()) +} + +pub fn get_active_network_adapters() -> AppResult> { + get_network_adapters().map(|adapters| { + adapters + .into_iter() + .filter(|adapter| adapter.is_up && !adapter.is_loopback) + .collect() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test; + + test! { + fn test_get_network_adapters() { + let adapters = get_network_adapters()?; + + assert!(!adapters.is_empty(), "No network adapters found"); + + for adapter in &adapters { + assert!(!adapter.name.is_empty(), "Found adapter with empty name"); + } + + let up_adapters = adapters.iter().filter(|a| a.is_up).count(); + assert!(up_adapters > 0, "No adapters are up"); + + let non_loopback = adapters.iter().filter(|a| !a.is_loopback).count(); + assert!(non_loopback > 0, "All adapters are loopback interfaces"); + } + } + + test! { + fn test_get_active_network_adapters() { + let adapters = get_active_network_adapters()?; + + assert!(!adapters.is_empty(), "No active network adapters found"); + + for adapter in &adapters { + assert!(adapter.is_up, "Found inactive adapter: {}", adapter.name); + assert!(!adapter.is_loopback, "Found loopback adapter: {}", adapter.name); + } + } + } + + test! { + fn test_network_adapter_display() { + let adapter = NetworkAdapter { + name: "eth0".to_string(), + is_up: true, + is_loopback: false, + }; + + assert_eq!(format!("{}", adapter), "eth0"); + } + } +} -- cgit v1.2.3