diff options
Diffstat (limited to 'src/ip_service.rs')
| -rw-r--r-- | src/ip_service.rs | 234 |
1 files changed, 194 insertions, 40 deletions
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)) + } + + + } |
