2024-04-24 23:41:42 -07:00
|
|
|
extern crate hyper;
|
|
|
|
|
extern crate serde;
|
|
|
|
|
|
2024-06-14 20:26:56 -07:00
|
|
|
use std::{path::PathBuf, str};
|
2024-06-01 18:17:57 -07:00
|
|
|
use log::{error};
|
2024-04-24 23:41:42 -07:00
|
|
|
|
2024-06-14 20:26:56 -07:00
|
|
|
use hyper::{Body, Client, Method, Request, Uri};
|
2024-06-01 18:16:25 -07:00
|
|
|
|
2024-04-24 23:41:42 -07:00
|
|
|
use async_trait::async_trait;
|
2024-06-14 20:23:44 -07:00
|
|
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
2024-06-01 18:17:57 -07:00
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
use crate::{model::{Conversation, JwtToken}, APIInterface};
|
2024-04-24 23:41:42 -07:00
|
|
|
|
|
|
|
|
type HttpClient = Client<hyper::client::HttpConnector>;
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
pub struct HTTPAPIClient {
|
2024-04-24 23:41:42 -07:00
|
|
|
pub base_url: Uri,
|
2024-06-14 20:23:44 -07:00
|
|
|
credentials: Option<Credentials>,
|
|
|
|
|
auth_token: Option<JwtToken>,
|
2024-04-24 23:41:42 -07:00
|
|
|
client: HttpClient,
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
|
|
|
pub struct Credentials {
|
|
|
|
|
pub username: String,
|
|
|
|
|
pub password: String,
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-24 23:41:42 -07:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum Error {
|
|
|
|
|
ClientError(String),
|
|
|
|
|
HTTPError(hyper::Error),
|
|
|
|
|
SerdeError(serde_json::Error),
|
2024-06-01 18:16:25 -07:00
|
|
|
DecodeError,
|
2024-04-24 23:41:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From <hyper::Error> for Error {
|
|
|
|
|
fn from(err: hyper::Error) -> Error {
|
|
|
|
|
Error::HTTPError(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From <serde_json::Error> for Error {
|
|
|
|
|
fn from(err: serde_json::Error) -> Error {
|
|
|
|
|
Error::SerdeError(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
trait AuthBuilder {
|
|
|
|
|
fn with_auth(self, token: &Option<JwtToken>) -> Self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AuthBuilder for hyper::http::request::Builder {
|
|
|
|
|
fn with_auth(self, token: &Option<JwtToken>) -> Self {
|
|
|
|
|
if let Some(token) = &token {
|
|
|
|
|
self.header("Authorization", token.to_header_value())
|
|
|
|
|
} else { self }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trait AuthSetting {
|
|
|
|
|
fn authenticate(&mut self, token: &Option<JwtToken>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<B> AuthSetting for hyper::http::Request<B> {
|
|
|
|
|
fn authenticate(&mut self, token: &Option<JwtToken>) {
|
|
|
|
|
if let Some(token) = &token {
|
|
|
|
|
self.headers_mut().insert("Authorization", token.to_header_value());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-24 23:41:42 -07:00
|
|
|
#[async_trait]
|
2024-06-14 20:23:44 -07:00
|
|
|
impl APIInterface for HTTPAPIClient {
|
2024-04-24 23:41:42 -07:00
|
|
|
type Error = Error;
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
async fn get_version(&mut self) -> Result<String, Self::Error> {
|
2024-04-24 23:41:42 -07:00
|
|
|
let version: String = self.request("/version", Method::GET).await?;
|
|
|
|
|
Ok(version)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
async fn get_conversations(&mut self) -> Result<Vec<Conversation>, Self::Error> {
|
2024-04-24 23:41:42 -07:00
|
|
|
let conversations: Vec<Conversation> = self.request("/conversations", Method::GET).await?;
|
|
|
|
|
Ok(conversations)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
async fn authenticate(&mut self, credentials: Credentials) -> Result<JwtToken, Self::Error> {
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
|
struct AuthResponse {
|
|
|
|
|
jwt: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let body = || -> Body { serde_json::to_string(&credentials).unwrap().into() };
|
|
|
|
|
let token: AuthResponse = self.request_with_body_retry("/authenticate", Method::POST, body, false).await?;
|
|
|
|
|
let token = JwtToken::new(&token.jwt).map_err(|_| Error::DecodeError)?;
|
|
|
|
|
self.auth_token = Some(token.clone());
|
|
|
|
|
Ok(token)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-01 18:16:25 -07:00
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
impl HTTPAPIClient {
|
|
|
|
|
pub fn new(base_url: Uri, credentials: Option<Credentials>) -> HTTPAPIClient {
|
|
|
|
|
HTTPAPIClient {
|
2024-06-14 20:26:56 -07:00
|
|
|
base_url,
|
|
|
|
|
credentials,
|
2024-06-14 20:23:44 -07:00
|
|
|
auth_token: Option::None,
|
|
|
|
|
client: Client::new(),
|
2024-06-01 18:16:25 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-24 23:41:42 -07:00
|
|
|
fn uri_for_endpoint(&self, endpoint: &str) -> Uri {
|
|
|
|
|
let mut parts = self.base_url.clone().into_parts();
|
|
|
|
|
let root_path: PathBuf = parts.path_and_query.unwrap().path().into();
|
|
|
|
|
let path = root_path.join(endpoint);
|
|
|
|
|
parts.path_and_query = Some(path.to_str().unwrap().parse().unwrap());
|
|
|
|
|
|
|
|
|
|
Uri::try_from(parts).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
async fn request<T: DeserializeOwned>(&mut self, endpoint: &str, method: Method) -> Result<T, Error> {
|
|
|
|
|
self.request_with_body(endpoint, method, || { Body::empty() }).await
|
2024-04-24 23:41:42 -07:00
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
async fn request_with_body<T, B>(&mut self, endpoint: &str, method: Method, body_fn: B) -> Result<T, Error>
|
|
|
|
|
where T: DeserializeOwned, B: Fn() -> Body
|
|
|
|
|
{
|
|
|
|
|
self.request_with_body_retry(endpoint, method, body_fn, true).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn request_with_body_retry<T, B>(
|
|
|
|
|
&mut self,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
method: Method,
|
|
|
|
|
body_fn: B,
|
|
|
|
|
retry_auth: bool) -> Result<T, Error>
|
|
|
|
|
where
|
|
|
|
|
T: DeserializeOwned,
|
|
|
|
|
B: Fn() -> Body
|
|
|
|
|
{
|
|
|
|
|
use hyper::StatusCode;
|
|
|
|
|
|
2024-04-24 23:41:42 -07:00
|
|
|
let uri = self.uri_for_endpoint(endpoint);
|
2024-06-14 20:23:44 -07:00
|
|
|
let build_request = move |auth: &Option<JwtToken>| {
|
|
|
|
|
let body = body_fn();
|
|
|
|
|
Request::builder()
|
|
|
|
|
.method(&method)
|
|
|
|
|
.uri(&uri)
|
|
|
|
|
.with_auth(auth)
|
|
|
|
|
.body(body)
|
|
|
|
|
.expect("Unable to build request")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let request = build_request(&self.auth_token);
|
|
|
|
|
let mut response = self.client.request(request).await?;
|
|
|
|
|
match response.status() {
|
|
|
|
|
StatusCode::OK => { /* cool */ },
|
|
|
|
|
|
|
|
|
|
// 401: Unauthorized. Token may have expired or is invalid. Attempt to renew.
|
|
|
|
|
StatusCode::UNAUTHORIZED => {
|
|
|
|
|
if !retry_auth {
|
|
|
|
|
return Err(Error::ClientError("Unauthorized".into()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(credentials) = &self.credentials {
|
|
|
|
|
self.authenticate(credentials.clone()).await?;
|
|
|
|
|
|
|
|
|
|
let request = build_request(&self.auth_token);
|
|
|
|
|
response = self.client.request(request).await?;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Other errors: bubble up.
|
|
|
|
|
_ => {
|
|
|
|
|
let message = format!("Request failed ({:})", response.status());
|
|
|
|
|
return Err(Error::ClientError(message));
|
|
|
|
|
}
|
2024-04-24 23:41:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read and parse response body
|
2024-06-14 20:23:44 -07:00
|
|
|
let body = hyper::body::to_bytes(response.into_body()).await?;
|
2024-06-01 18:16:25 -07:00
|
|
|
let parsed: T = match serde_json::from_slice(&body) {
|
|
|
|
|
Ok(result) => Ok(result),
|
|
|
|
|
Err(json_err) => {
|
|
|
|
|
// If JSON deserialization fails, try to interpret it as plain text
|
|
|
|
|
// Unfortunately the server does return things like this...
|
|
|
|
|
let s = str::from_utf8(&body).map_err(|_| Error::DecodeError)?;
|
|
|
|
|
serde_plain::from_str(s).map_err(|_| json_err)
|
|
|
|
|
}
|
|
|
|
|
}?;
|
2024-04-24 23:41:42 -07:00
|
|
|
|
|
|
|
|
Ok(parsed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod test {
|
|
|
|
|
use super::*;
|
2024-06-01 18:16:25 -07:00
|
|
|
use ctor::ctor;
|
|
|
|
|
|
|
|
|
|
#[ctor]
|
|
|
|
|
fn init() {
|
|
|
|
|
pretty_env_logger::init();
|
|
|
|
|
log::set_max_level(log::LevelFilter::Trace);
|
|
|
|
|
}
|
2024-04-24 23:41:42 -07:00
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
fn local_mock_client() -> HTTPAPIClient {
|
2024-04-24 23:41:42 -07:00
|
|
|
let base_url = "http://localhost:5738".parse().unwrap();
|
2024-06-14 20:23:44 -07:00
|
|
|
let credentials = Credentials {
|
|
|
|
|
username: "test".to_string(),
|
|
|
|
|
password: "test".to_string(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
HTTPAPIClient::new(base_url, credentials.into())
|
2024-04-24 23:41:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn mock_client_is_reachable() -> bool {
|
2024-06-14 20:23:44 -07:00
|
|
|
let mut client = local_mock_client();
|
2024-04-24 23:41:42 -07:00
|
|
|
let version = client.get_version().await;
|
2024-06-01 18:16:25 -07:00
|
|
|
|
|
|
|
|
match version {
|
|
|
|
|
Ok(_) => true,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Mock client error: {:?}", e);
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-24 23:41:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_version() {
|
|
|
|
|
if !mock_client_is_reachable().await {
|
2024-06-01 18:16:25 -07:00
|
|
|
log::warn!("Skipping http_client tests (mock server not reachable)");
|
2024-04-24 23:41:42 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
let mut client = local_mock_client();
|
2024-04-24 23:41:42 -07:00
|
|
|
let version = client.get_version().await.unwrap();
|
|
|
|
|
assert!(version.starts_with("KordophoneMock-"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_conversations() {
|
|
|
|
|
if !mock_client_is_reachable().await {
|
2024-06-01 18:16:25 -07:00
|
|
|
log::warn!("Skipping http_client tests (mock server not reachable)");
|
2024-04-24 23:41:42 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 20:23:44 -07:00
|
|
|
let mut client = local_mock_client();
|
2024-04-24 23:41:42 -07:00
|
|
|
let conversations = client.get_conversations().await.unwrap();
|
2024-06-01 18:17:57 -07:00
|
|
|
assert!(!conversations.is_empty());
|
2024-04-24 23:41:42 -07:00
|
|
|
}
|
|
|
|
|
}
|