aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs4
-rw-r--r--src/ip_service.rs234
-rw-r--r--src/main.rs13
3 files changed, 201 insertions, 50 deletions
diff --git a/src/config.rs b/src/config.rs
index f098838..88fb280 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,5 +1,5 @@
use std::{fs, io};
-use std::path::{Path, PathBuf};
+use std::path::Path;
use crate::dyndns_service::DynDnsProvider;
use fqdn::FQDN;
use serde::{Deserialize, Serialize};
@@ -53,7 +53,7 @@ pub enum DnsRecordType {
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
-struct DnsRecord {
+pub struct DnsRecord {
#[serde_as(as = "DisplayFromStr")]
fqdn: FQDN,
#[serde(default = "default_ttl")]
diff --git a/src/ip_service.rs b/src/ip_service.rs
index ee1a092..1d7f253 100644
--- a/src/ip_service.rs
+++ b/src/ip_service.rs
@@ -1,16 +1,26 @@
+use crate::error::{AppError, AppResult};
+use crate::ip_service::IpServiceProvider::IDENTME;
+use http::StatusCode;
+use reqwest::{Client, Url};
+use serde::{Deserialize, Serialize};
+use serde_with::serde_as;
use serde_with::DisplayFromStr;
-use reqwest::Url;
use std::error::Error;
use std::net::IpAddr;
use std::str::FromStr;
-use serde::{Deserialize, Serialize};
-use serde_with::serde_as;
-use crate::ip_service::IpServiceProvider::IDENTME;
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "type")]
pub enum IpServiceProvider {
- IDENTME(IdentMe)
+ IDENTME(IdentMe),
+}
+
+impl IpService for IpServiceProvider {
+ async fn resolve(&self, client: &Client) -> AppResult<IpAddr> {
+ match self {
+ IDENTME(ident_me) => ident_me.resolve(client).await,
+ }
+ }
}
impl Default for IpServiceProvider {
@@ -19,15 +29,17 @@ impl Default for IpServiceProvider {
}
}
-pub struct IpService {}
-
-impl IpService {
- pub(crate) async fn resolve(config: &impl IpServiceConfiguration) -> Result<IpAddr, Box<dyn Error>> {
- let response = reqwest::get(config.get_service_url()).await.unwrap();
- Ok(IpAddr::from_str(&response.text().await.unwrap())?)
- }
+pub trait IpService {
+ async fn resolve(&self, client: &Client) -> AppResult<IpAddr>;
}
+// impl IpService {
+// pub(crate) async fn resolve(config: &impl IpServiceConfiguration) -> Result<IpAddr, Box<dyn Error>> {
+// let response = reqwest::get(config.get_service_url()).await.unwrap();
+// Ok(IpAddr::from_str(&response.text().await.unwrap())?)
+// }
+// }
+
pub trait IpServiceConfiguration {
fn get_service_url(&self) -> Url;
}
@@ -36,12 +48,67 @@ pub trait IpServiceConfiguration {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct IdentMe {
#[serde_as(as = "DisplayFromStr")]
- url: Url
+ url: Url,
+}
+
+impl IdentMe {
+ fn convert_string_to_ip_address(input: String, url: &Url) -> AppResult<IpAddr> {
+ IpAddr::from_str(&input).map_err(|e| AppError::InvalidResponse {
+ url: url.to_string(),
+ reason: format!(
+ "Failed to parse service response [{}] into valid ip address: {}",
+ input, e
+ ),
+ })
+ }
+
+ fn request_failed(&self, e: reqwest::Error) -> AppError {
+ AppError::RequestFailed {
+ url: self.url.to_string(),
+ source: e,
+ }
+ }
+
+ fn invalid_status_code(&self, status: StatusCode) -> AppError {
+ AppError::InvalidResponse {
+ url: self.url.to_string(),
+ reason: format!("Status code: {}", status),
+ }
+ }
+
+ fn missing_text_response(&self, e: reqwest::Error) -> AppError {
+ AppError::InvalidResponse {
+ url: self.url.to_string(),
+ reason: format!("Unable to get text from service response: {}", e),
+ }
+ }
}
impl Default for IdentMe {
fn default() -> Self {
- Self { url: Url::parse("https://v4.ident.me/").unwrap() }
+ Self {
+ url: Url::parse("https://v4.ident.me/").unwrap(),
+ }
+ }
+}
+
+impl IpService for IdentMe {
+ async fn resolve(&self, client: &Client) -> AppResult<IpAddr> {
+ let response = client
+ .get(self.url.clone())
+ .send()
+ .await
+ .map_err(|e| self.request_failed(e))?;
+
+ if !response.status().is_success() {
+ return Err(self.invalid_status_code(response.status()));
+ };
+
+ let response_text = response.text().await;
+ match response_text {
+ Ok(text) => Self::convert_string_to_ip_address(text, &self.url),
+ Err(e) => Err(self.missing_text_response(e)),
+ }
}
}
@@ -54,56 +121,143 @@ impl IpServiceConfiguration for IdentMe {
#[cfg(test)]
mod tests {
use super::*;
- use std::net::Ipv4Addr;
- use wiremock::matchers::{method, path};
- use wiremock::{Mock, MockServer, ResponseTemplate};
+ use crate::error::AppError::{InvalidResponse, RequestFailed};
+ use reqwest::ClientBuilder;
+ use wiremock::matchers::method;
+ use wiremock::{Mock, MockServer, Respond, ResponseTemplate};
- async fn setup_ipv4_service(service_path: &str, response: &str) -> MockServer {
+ async fn setup_ipv4_service(response: impl Respond + 'static) -> MockServer {
let service = MockServer::start().await;
Mock::given(method("GET"))
- .and(path(service_path))
- .respond_with(
- ResponseTemplate::new(200)
- .insert_header("Content-Type", "text/plain; charset=utf-8")
- .set_body_string(response),
- )
+ .respond_with(response)
.mount(&service)
.await;
service
}
+ fn plaintext_response(result: String) -> impl Respond + 'static {
+ ResponseTemplate::new(200)
+ .insert_header("Content-Type", "text/plain; charset=utf-8")
+ .set_body_string(result)
+ }
+
+ fn empty_response() -> impl Respond + 'static {
+ ResponseTemplate::new(204)
+ }
+
+ fn server_error() -> impl Respond + 'static {
+ ResponseTemplate::new(500)
+ }
+
#[tokio::test]
async fn successful_ipv4_address_resolution() -> Result<(), Box<dyn Error>> {
- let service_path = "get-my-ip-address";
- let service_response = "17.5.7.8";
+ let expected = IpAddr::from_str("17.5.7.8")?;
+ let input = plaintext_response(expected.to_string());
+ let (ip_service, actual) = run(input).await?;
- let ip_service = setup_ipv4_service(service_path, service_response).await;
- let service_config = MockConfig::new(&ip_service, service_path);
+ assert_eq!(1, ip_service.received_requests().await.unwrap().len());
+ assert_eq!(actual.unwrap(), expected);
- let actual = IpService::resolve(&service_config).await?;
- assert_eq!(actual, IpAddr::V4(Ipv4Addr::new(17, 5, 7, 8)));
+ Ok(())
+ }
+ #[tokio::test]
+ async fn failed_address_resolution_no_server() -> Result<(), Box<dyn Error>> {
+ let client = ClientBuilder::new().build()?;
+ let server_url = "http://localhost:8765/path";
+ let service = IdentMe {
+ url: server_url.parse()?,
+ };
- assert_eq!(1, ip_service.received_requests().await.unwrap().len());
+ let actual = IDENTME(service).resolve(&client).await;
+ assert!(actual.is_err());
+
+ match actual.err().unwrap() {
+ RequestFailed { url, .. } => {
+ assert_eq!(url, server_url.to_string());
+ }
+ _ => {
+ panic!("Unexpected error")
+ }
+ }
Ok(())
}
- struct MockConfig {
- service_url: Url,
+ #[tokio::test]
+ async fn failed_non_successful_status_code() -> Result<(), Box<dyn Error>> {
+ let input = server_error();
+ let (ip_service, actual) = run(input).await?;
+
+ assert_eq!(1, ip_service.received_requests().await.unwrap().len());
+ assert!(actual.is_err());
+
+ match actual.err().unwrap() {
+ InvalidResponse { url, reason } => {
+ assert_eq!(Url::parse(&*url), Url::from_str(&*ip_service.uri()));
+ assert_eq!(reason, "Status code: 500 Internal Server Error");
+ }
+ _ => {
+ panic!("Unexpected error")
+ }
+ }
+
+ Ok(())
}
- impl MockConfig {
- fn new(server: &MockServer, path: &str) -> Self {
- Self {
- service_url: Url::parse(format!("{}/{}", server.uri(), path).as_str()).unwrap(),
+ #[tokio::test]
+ async fn failed_non_successful_empty_response() -> Result<(), Box<dyn Error>> {
+ let input = empty_response();
+ let (ip_service, actual) = run(input).await?;
+
+ assert_eq!(1, ip_service.received_requests().await.unwrap().len());
+ assert!(actual.is_err());
+
+ match actual.err().unwrap() {
+ InvalidResponse { url, .. } => {
+ assert_eq!(Url::parse(&*url), Url::from_str(&*ip_service.uri()));
+ }
+ _ => {
+ panic!("Unexpected error")
}
}
+
+ Ok(())
}
- impl IpServiceConfiguration for MockConfig {
- fn get_service_url(&self) -> Url {
- self.service_url.clone()
+
+ #[tokio::test]
+ async fn failed_non_successful_not_an_ip_address() -> Result<(), Box<dyn Error>> {
+ let input = plaintext_response("not-an-ip-address".to_string());
+ let (ip_service, actual) = run(input).await?;
+
+ assert_eq!(1, ip_service.received_requests().await.unwrap().len());
+ assert!(actual.is_err());
+
+ match actual.err().unwrap() {
+ InvalidResponse { url, .. } => {
+ assert_eq!(Url::parse(&*url), Url::from_str(&*ip_service.uri()));
+ }
+ _ => {
+ panic!("Unexpected error")
+ }
}
+
+ Ok(())
}
+
+ async fn run(payload: impl Respond + 'static) -> Result<(MockServer, AppResult<IpAddr>), Box<dyn Error>> {
+ let ip_service = setup_ipv4_service(payload).await;
+
+ let client = ClientBuilder::new().build()?;
+ let service = IdentMe {
+ url: ip_service.uri().parse()?,
+ };
+
+ let actual = IDENTME(service).resolve(&client).await;
+ Ok((ip_service, actual))
+ }
+
+
+
}
diff --git a/src/main.rs b/src/main.rs
index 7fce580..06bea8e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,16 +1,13 @@
-use std::path::{Path, PathBuf};
-use std::process::exit;
-use clap::Parser;
-use log::info;
use crate::config::Config;
-use crate::dyndns_service::DynDnsService;
use crate::error::AppResult;
-use crate::ip_service::IpService;
+use clap::Parser;
+use std::path::PathBuf;
+use std::process::exit;
-mod dyndns_service;
-mod ip_service;
mod config;
+mod dyndns_service;
mod error;
+mod ip_service;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]