From 45d4f7ae39038912e756a6ba9503b63e8c86fc94 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Tue, 28 Oct 2025 17:29:35 +0000 Subject: [PATCH 1/4] Remove mock client (#17) --- Cargo.toml | 3 +++ src/clients.rs | 27 --------------------------- src/handlers.rs | 25 ++++++++++++++++++------- src/model.rs | 6 +++--- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f796262..2781c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,6 @@ async-graphql-axum = "7.0.17" url = "2.5.7" config = "0.15.16" clap = { version = "4.5.48", features = ["derive"] } + +[dev-dependencies] +httpmock = "0.8.2" diff --git a/src/clients.rs b/src/clients.rs index a47e264..e7ca8fa 100644 --- a/src/clients.rs +++ b/src/clients.rs @@ -1,8 +1,4 @@ use std::fmt; -#[cfg(test)] -use std::fs::File; -#[cfg(test)] -use std::path::PathBuf; use reqwest::Url; use serde::de::DeserializeOwned; @@ -37,29 +33,6 @@ impl Client for TiledClient { } } -#[cfg(test)] -pub struct MockTiledClient { - pub dir_path: PathBuf, -} - -#[cfg(test)] -impl MockTiledClient { - async fn deserialize_from_file(&self, filename: &str) -> ClientResult { - println!("Requesting data from mock"); - - let path = self.dir_path.join(filename); - let file = File::open(&path)?; - - Ok(serde_json::from_reader(file)?) - } -} -#[cfg(test)] -impl Client for MockTiledClient { - async fn metadata(&self) -> ClientResult { - self.deserialize_from_file("tiled_metadata.json").await - } -} - #[derive(Debug)] pub enum ClientError { Parse(url::ParseError), diff --git a/src/handlers.rs b/src/handlers.rs index 5b92798..55d437d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -8,7 +8,7 @@ use crate::clients::Client; use crate::model::TiledQuery; pub async fn graphql_handler( - schema: Extension, EmptyMutation, EmptySubscription>>, + schema: Extension>, req: GraphQLRequest, ) -> GraphQLResponse { let query = req.into_inner().query; @@ -23,15 +23,27 @@ pub async fn graphiql_handler() -> impl IntoResponse { #[cfg(test)] mod tests { use async_graphql::{EmptyMutation, EmptySubscription, Schema}; + use httpmock::MockServer; + use url::Url; use crate::TiledQuery; - use crate::clients::MockTiledClient; + use crate::clients::TiledClient; #[tokio::test] async fn test_api_version_query() { + let mock_server = MockServer::start(); + + let mock = mock_server + .mock_async(|when, then| { + when.method("GET").path("/api/v1/"); + then.status(200) + .body_from_file("resources/tiled_metadata.json"); + }) + .await; + let schema = Schema::build( - TiledQuery(MockTiledClient { - dir_path: "./resources".into(), + TiledQuery(TiledClient { + address: Url::parse(&mock_server.base_url()).unwrap(), }), EmptyMutation, EmptySubscription, @@ -40,8 +52,7 @@ mod tests { let response = schema.execute("{metadata { apiVersion } }").await; - println!("{:?}", response.data.to_string()); - - assert!(response.data.to_string() == "{metadata: {apiVersion: 0}}") + assert_eq!(response.data.to_string(), "{metadata: {apiVersion: 0}}"); + mock.assert(); } } diff --git a/src/model.rs b/src/model.rs index b44a16d..3dc83e8 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,12 +2,12 @@ pub(crate) mod metadata; use async_graphql::Object; -use crate::clients::{Client, ClientError}; +use crate::clients::{Client, ClientError, TiledClient}; -pub(crate) struct TiledQuery(pub T); +pub(crate) struct TiledQuery(pub TiledClient); #[Object] -impl TiledQuery { +impl TiledQuery { async fn metadata(&self) -> async_graphql::Result { self.0.metadata().await } From a742373cec3753b9d66661be6137e74a5b4d8cef Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Tue, 28 Oct 2025 17:58:13 +0000 Subject: [PATCH 2/4] Rename client error variants (#18) --- src/clients.rs | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/clients.rs b/src/clients.rs index e7ca8fa..1754ac0 100644 --- a/src/clients.rs +++ b/src/clients.rs @@ -18,13 +18,10 @@ pub struct TiledClient { impl TiledClient { async fn request(&self, endpoint: &str) -> ClientResult { println!("Requesting data from tiled"); - let url = self.address.join(endpoint)?; - - let response = reqwest::get(url).await?; - let json = response.json().await?; - - Ok(serde_json::from_value(json)?) + let response = reqwest::get(url).await?.error_for_status()?; + let body = response.text().await?; + serde_json::from_str(&body).map_err(|e| ClientError::InvalidResponse(e, body)) } } impl Client for TiledClient { @@ -35,38 +32,29 @@ impl Client for TiledClient { #[derive(Debug)] pub enum ClientError { - Parse(url::ParseError), - Reqwest(reqwest::Error), - Serde(serde_json::Error), - Io(std::io::Error), + InvalidPath(url::ParseError), + ServerError(reqwest::Error), + InvalidResponse(serde_json::Error, String), } impl From for ClientError { fn from(err: url::ParseError) -> ClientError { - ClientError::Parse(err) + ClientError::InvalidPath(err) } } impl From for ClientError { fn from(err: reqwest::Error) -> ClientError { - ClientError::Reqwest(err) - } -} -impl From for ClientError { - fn from(err: serde_json::Error) -> ClientError { - ClientError::Serde(err) - } -} -impl From for ClientError { - fn from(err: std::io::Error) -> ClientError { - ClientError::Io(err) + ClientError::ServerError(err) } } + impl std::fmt::Display for ClientError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ClientError::Parse(ref err) => write!(f, "Parse error: {}", err), - ClientError::Reqwest(ref err) => write!(f, "Request error: {}", err), - ClientError::Serde(ref err) => write!(f, "Serde error: {}", err), - ClientError::Io(ref err) => write!(f, "IO Error: {}", err), + match self { + ClientError::InvalidPath(err) => write!(f, "Invalid URL path: {}", err), + ClientError::ServerError(err) => write!(f, "Tiled server error: {}", err), + ClientError::InvalidResponse(err, actual) => { + write!(f, "Invalid response: {err}, response: {actual}") + } } } } From 5bcd0a29504c79ab8527e1fa3cebe54dccc7b6d0 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Tue, 28 Oct 2025 18:17:37 +0000 Subject: [PATCH 3/4] Move test into model (#19) --- src/handlers.rs | 37 ------------------------------------- src/model.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 55d437d..b0270fe 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -19,40 +19,3 @@ pub async fn graphql_handler( pub async fn graphiql_handler() -> impl IntoResponse { Html(GraphiQLSource::build().endpoint("/graphql").finish()) } - -#[cfg(test)] -mod tests { - use async_graphql::{EmptyMutation, EmptySubscription, Schema}; - use httpmock::MockServer; - use url::Url; - - use crate::TiledQuery; - use crate::clients::TiledClient; - - #[tokio::test] - async fn test_api_version_query() { - let mock_server = MockServer::start(); - - let mock = mock_server - .mock_async(|when, then| { - when.method("GET").path("/api/v1/"); - then.status(200) - .body_from_file("resources/tiled_metadata.json"); - }) - .await; - - let schema = Schema::build( - TiledQuery(TiledClient { - address: Url::parse(&mock_server.base_url()).unwrap(), - }), - EmptyMutation, - EmptySubscription, - ) - .finish(); - - let response = schema.execute("{metadata { apiVersion } }").await; - - assert_eq!(response.data.to_string(), "{metadata: {apiVersion: 0}}"); - mock.assert(); - } -} diff --git a/src/model.rs b/src/model.rs index 3dc83e8..f6b88b1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -12,3 +12,40 @@ impl TiledQuery { self.0.metadata().await } } + +#[cfg(test)] +mod tests { + use async_graphql::{EmptyMutation, EmptySubscription, Schema}; + use httpmock::MockServer; + use url::Url; + + use crate::TiledQuery; + use crate::clients::TiledClient; + + #[tokio::test] + async fn test_api_version_query() { + let mock_server = MockServer::start(); + + let mock = mock_server + .mock_async(|when, then| { + when.method("GET").path("/api/v1/"); + then.status(200) + .body_from_file("resources/tiled_metadata.json"); + }) + .await; + + let schema = Schema::build( + TiledQuery(TiledClient { + address: Url::parse(&mock_server.base_url()).unwrap(), + }), + EmptyMutation, + EmptySubscription, + ) + .finish(); + + let response = schema.execute("{metadata { apiVersion } }").await; + + assert_eq!(response.data.to_string(), "{metadata: {apiVersion: 0}}"); + mock.assert(); + } +} From 3af95cecd0f9e2d50705c3f28e0325d741bf002a Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Wed, 29 Oct 2025 10:54:21 +0000 Subject: [PATCH 4/4] Add tests for errors (#20) --- src/model.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/src/model.rs b/src/model.rs index f6b88b1..b953ebc 100644 --- a/src/model.rs +++ b/src/model.rs @@ -15,7 +15,7 @@ impl TiledQuery { #[cfg(test)] mod tests { - use async_graphql::{EmptyMutation, EmptySubscription, Schema}; + use async_graphql::{EmptyMutation, EmptySubscription, Schema, Value}; use httpmock::MockServer; use url::Url; @@ -25,7 +25,6 @@ mod tests { #[tokio::test] async fn test_api_version_query() { let mock_server = MockServer::start(); - let mock = mock_server .mock_async(|when, then| { when.method("GET").path("/api/v1/"); @@ -46,6 +45,88 @@ mod tests { let response = schema.execute("{metadata { apiVersion } }").await; assert_eq!(response.data.to_string(), "{metadata: {apiVersion: 0}}"); + assert_eq!(response.errors, &[]); + mock.assert(); + } + + #[tokio::test] + async fn test_server_unavailable() { + let schema = Schema::build( + TiledQuery(TiledClient { + address: Url::parse("http://tiled.example.com").unwrap(), + }), + EmptyMutation, + EmptySubscription, + ) + .finish(); + + let response = schema.execute("{metadata { apiVersion } }").await; + assert_eq!(response.data, Value::Null); + assert_eq!( + response.errors[0].message, + "Tiled server error: error sending request for url (http://tiled.example.com/api/v1/)" + ); + assert_eq!(response.errors.len(), 1); + } + + #[tokio::test] + async fn test_internal_tiled_error() { + let mock_server = MockServer::start(); + let mock = mock_server + .mock_async(|when, then| { + when.method("GET").path("/api/v1/"); + then.status(503); + }) + .await; + + let schema = Schema::build( + TiledQuery(TiledClient { + address: Url::parse(&mock_server.base_url()).unwrap(), + }), + EmptyMutation, + EmptySubscription, + ) + .finish(); + + let response = schema.execute("{metadata { apiVersion } }").await; + let actual = &response.errors[0].message; + let expected = + "Tiled server error: HTTP status server error (503 Service Unavailable) for url"; + assert_eq!(response.data, Value::Null); + assert!( + actual.starts_with(expected), + "Unexpected error: {actual} \nExpected: {expected} [...]" + ); + assert_eq!(response.errors.len(), 1); + mock.assert(); + } + + #[tokio::test] + async fn test_invalid_server_response() { + let mock_server = MockServer::start(); + let mock = mock_server + .mock_async(|when, then| { + when.method("GET").path("/api/v1/"); + then.status(200).body("{}"); + }) + .await; + + let schema = Schema::build( + TiledQuery(TiledClient { + address: Url::parse(&mock_server.base_url()).unwrap(), + }), + EmptyMutation, + EmptySubscription, + ) + .finish(); + + let response = schema.execute("{metadata { apiVersion } }").await; + assert_eq!(response.data, Value::Null); + assert_eq!(response.errors.len(), 1); + assert_eq!( + response.errors[0].message, + "Invalid response: missing field `api_version` at line 1 column 2, response: {}" + ); mock.assert(); } }