aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGus Power <gus@infinitesidequests.com>2025-06-07 14:55:21 +0100
committerGus Power <gus@infinitesidequests.com>2025-06-07 14:55:21 +0100
commit172f8163139f8112b76d462198a1213a5cb49dde (patch)
tree0a10558a0b2a4ce4e07177bc520b6f6030553763 /src
parent3e5aa28345bb009c12b5a55f2e7174957bf4ed9a (diff)
first test of Junie - add network module for detecting available network adapters
Diffstat (limited to 'src')
-rw-r--r--src/error.rs12
-rw-r--r--src/http.rs46
-rw-r--r--src/lib.rs11
-rw-r--r--src/main.rs3
-rw-r--r--src/network.rs93
5 files changed, 153 insertions, 12 deletions
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() {
@@ -73,6 +83,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")?;
let invalid_headers = vec![("Content\x00Type".to_string(), "application/json".to_string())];
@@ -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<NetworkInterface> 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<Vec<NetworkAdapter>> {
+ NetworkInterface::show()
+ .map_err(AppError::NetworkInterfaceError)
+ .map(|interfaces| interfaces.into_iter().map(NetworkAdapter::from).collect())
+}
+
+pub fn get_active_network_adapters() -> AppResult<Vec<NetworkAdapter>> {
+ 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");
+ }
+ }
+}