From dc74920dc8a1f5f403b131f95d8caacc936d35fa Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 10 Nov 2024 17:11:54 +0000 Subject: [PATCH] feat(net): cleaner mock.on syntax --- examples/get.rs | 7 +-- justfile | 6 ++ src/net/mod.rs | 150 +++++++++++++++++++++++++++++++++++++++++++++- src/net/system.rs | 82 ++++++++++++++++++++----- tests/net.rs | 89 ++++++++------------------- 5 files changed, 248 insertions(+), 86 deletions(-) diff --git a/examples/get.rs b/examples/get.rs index f97cc29..5d91e88 100644 --- a/examples/get.rs +++ b/examples/get.rs @@ -114,14 +114,13 @@ mod tests { let url = "http://localhost:8080"; // declare what response should be made for a given request - let response: http::Response<&str> = - mock_net.response().body("contents").expect("response body"); - let request = mock_net.client().get(url).build().expect("request"); + let response = mock_net.response().body("contents"); + let request = mock_net.client().get(url); mock_net .on(request) // By default, the METHOD and URL must match, equivalent to: //.match_on(vec![MatchOn::Method, MatchOn::Url]) - .respond(response) + .respond_with_body(response) .expect("mock"); // Create a temporary directory that will be deleted with `fs` goes out of scope diff --git a/justfile b/justfile index 9ee25d7..e4b45f5 100644 --- a/justfile +++ b/justfile @@ -10,6 +10,12 @@ build: cargo test --example get cargo mutants --jobs 4 +doc-test: + cargo doc + cargo test + cargo test --example get + + install-hooks: @echo "Installing git hooks" git config core.hooksPath .git-hooks diff --git a/src/net/mod.rs b/src/net/mod.rs index 89691a5..1161184 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1,13 +1,161 @@ //! Provides a generic interface for network operations. //! //! +//! Provides a testable interface over the [reqwest] crate. +//! +//! ## Overview +//! +//! The `net` module provides a testable interface for network operations. +//! It includes implementations for both real network interactions and mocked network operations for testing purposes. +//! +//! ## Key methods and types: +//! +//! - [kxio::net::new()][new()]: Creates a new `Net` instance +//! - [kxio::net::mock()][mock()]: Creates a new `MockNet` instance for use in tests +//! - [Error]: enum for network-related errors +//! - [Result]: an alias for `core::result::Result` +//! - [Net]: struct for real and mocked network operations +//! - [MockNet]: struct for defining behaviours of mocked network operations +//! +//! ## Usage +//! +//! Write your program to take a reference to [Net]. +//! +//! Use the [Net::client] functionto create a [reqwest::RequestBuilder] which you should then pass to the [Net::send] method. +//! This is rather than building the request and calling its own `send` method, doing so would result in the network request being sent, even under-test. +//! +//! ```rust +//! use kxio::net; +//! async fn get_example(net: &net::Net) -> net::Result<()> { +//! let response = net.send(net.client().get("https://example.com")).await?; +//! ///... +//! Ok(()) +//! } +//! ``` +//! +//! ### Real Network Operations +//! +//! In your production code you will want to make real network requests. +//! +//! Construct a [Net] using [kxio::net::new()][new()]. Then pass as a reference to your program. +//! +//! ```rust +//! use kxio::net; +//! # #[tokio::main] +//! # async fn main() -> net::Result<()> { +//! let net = net::new(); +//! +//! get_example(&net).await?; +//! # Ok(()) +//! # } +//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())} +//! ``` +//! +//! ### Mocked Network Operations +//! +//! In your tests you will want to mock your network requests and responses. +//! +//! Construct a [MockNet] using [kxio::net::mock()][mock()]. +//! +//! ```rust +//! use kxio::net; +//! let mock_net = net::mock(); +//! ``` +//! +//! Create a [reqwest::Client] using [MockNet::client()]. +//! +//! ```rust +//! # let mock_net = kxio::net::mock(); +//! let client = mock_net.client(); +//! // this is the same as: +//! let client = reqwest::Client::new(); +//! ``` +//! +//! Define the expected responses for each request, using the [MockNet::on], +//! that you expect you program to make during the test. You can choose what each request should be +//! matched against. The default is to the match when both the Method and Url are the same. +//! +//! ```rust +//! use kxio::net; +//! use kxio::net::MatchOn; +//! +//! # #[tokio::main] +//! # async fn main() -> net::Result<()> { +//! # let mock_net = net::mock(); +//! # let client = mock_net.client(); +//! mock_net.on(client.get("https://example.com")) +//! .match_on(vec![ +//! MatchOn::Url, +//! MatchOn::Method +//! ]) +//! .respond(mock_net.response().status(200))?; +//! mock_net.on(client.get("https://example.com/foo")) +//! .match_on(vec![ +//! MatchOn::Url, +//! MatchOn::Method +//! ]) +//! .respond_with_body(mock_net.response().status(500).body("Mocked response"))?; +//! # mock_net.reset(); +//! # Ok(()) +//! # } +//! ``` +//! +//! All [MatchOn] options: +//! - [MatchOn::Method] (default) +//! - [MatchOn::Url] (default) +//! - [MatchOn::Headers] +//! - [MatchOn::Body]. +//! +//! Once you have defined all your expected responses, convert the [MockNet] into a [Net]. +//! +//! ```rust +//! # use kxio::net; +//! # let mock_net = net::mock(); +//! let net: net::Net = mock_net.into(); +//! // or +//! # let mock_net = net::mock(); +//! let net = net::Net::from(mock_net); +//! ``` +//! +//! Now you can pass a reference to `net` to your program. +//! +//! ```rust +//! # use kxio::net; +//! # #[tokio::main] +//! # async fn main() -> net::Result<()> { +//! # let mock_net = net::mock(); +//! # let net = net::Net::from(mock_net); +//! get_example(&net).await?; +//! # Ok(()) +//! # } +//! # async fn get_example(net: &net::Net) -> net::Result<()> {Ok(())} +//! ``` +//! +//! When your test is finished, the [MockNet] will check that all the expected requests were +//! actually made. If there were any missed, then the test will [panic]. +//! +//! If you don't want this to happen, then call [MockNet::reset] before your test finishes. +//! You will need to recover the [MockNet] from the [Net]. +//! +//! ```rust +//! # use kxio::net; +//! # let mock_net = net::mock(); +//! # let net = net::Net::from(mock_net); +//! let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock"); +//! mock_net.reset(); +//! ```` +//! +//! ## Error Handling +//! +//! The module uses a custom [Result] type that wraps `core::result::Result` with the custom [Error] enum, +//! allowing for specific error handling related to network operations. mod result; mod system; pub use result::{Error, Result}; -pub use system::{MatchOn, MockNet, Net}; +pub use system::{MatchOn, MockNet, Net, OnRequest}; /// Creates a new `Net`. pub const fn new() -> Net { diff --git a/src/net/system.rs b/src/net/system.rs index 1ed229e..6374e74 100644 --- a/src/net/system.rs +++ b/src/net/system.rs @@ -32,6 +32,7 @@ struct Plan { match_on: Vec, } +/// An abstraction for the network #[derive(Debug)] pub struct Net { inner: InnerNet, @@ -68,6 +69,14 @@ impl Net { } } } +impl Default for Net { + fn default() -> Self { + Self { + inner: InnerNet::::new(), + mock: None, + } + } +} impl TryFrom for MockNet { type Error = super::Error; @@ -87,7 +96,7 @@ impl MockNet { pub fn client(&self) -> Client { Default::default() } - pub fn on(&self, request: impl Into) -> OnRequest { + pub fn on(&self, request: impl Into) -> OnRequest { self.inner.on(request) } /// Creates a [http::response::Builder] to be extended and returned by a mocked network request. @@ -190,11 +199,14 @@ impl InnerNet { } } - pub fn on(&self, request: impl Into) -> OnRequest { - OnRequest { - net: self, - request: request.into(), - match_on: vec![MatchOn::Method, MatchOn::Url], + pub fn on(&self, request_builder: impl Into) -> OnRequest { + match request_builder.into().build() { + Ok(request) => OnRequest::Valid { + net: self, + request, + match_on: vec![MatchOn::Method, MatchOn::Url], + }, + Err(err) => OnRequest::Error(err.into()), } } @@ -228,20 +240,58 @@ impl Drop for InnerNet { } } -pub struct OnRequest<'net> { - net: &'net InnerNet, - request: reqwest::Request, - match_on: Vec, +pub enum OnRequest<'net> { + Valid { + net: &'net InnerNet, + request: reqwest::Request, + match_on: Vec, + }, + Error(super::Error), } impl<'net> OnRequest<'net> { pub fn match_on(self, match_on: Vec) -> Self { - Self { - net: self.net, - request: self.request, - match_on, + if let OnRequest::Valid { + net, + request, + match_on: _, + } = self + { + Self::Valid { + net, + request, + match_on, + } + } else { + self } } - pub fn respond(self, response: impl Into) -> Result<()> { - self.net._on(self.request, response.into(), self.match_on) + pub fn respond(self, response: impl Into) -> Result<()> { + match self { + OnRequest::Valid { + net: _, + request: _, + match_on: _, + } => self.respond_with_body(response.into().body("")), + OnRequest::Error(error) => Err(error), + } + } + pub fn respond_with_body( + self, + body_result: std::result::Result, http::Error>, + ) -> Result<()> + where + T: Into, + { + match body_result { + Ok(response) => match self { + OnRequest::Valid { + net, + request, + match_on, + } => net._on(request, response.into(), match_on), + OnRequest::Error(error) => Err(error), + }, + Err(err) => Err(err.into()), + } } } diff --git a/tests/net.rs b/tests/net.rs index 5648463..7e608f8 100644 --- a/tests/net.rs +++ b/tests/net.rs @@ -9,15 +9,11 @@ async fn test_get_url() { let client = net.client(); let url = "https://www.example.com"; - let request = client.get(url).build().expect("build request"); - let my_response = net - .response() - .status(200) - .body("Get OK") - .expect("request body"); + let request = client.get(url); + let my_response = net.response().status(200).body("Get OK"); net.on(request) - .respond(my_response) + .respond_with_body(my_response) .expect("on request, respond"); //when @@ -38,15 +34,11 @@ async fn test_get_wrong_url() { let client = net.client(); let url = "https://www.example.com"; - let request = client.get(url).build().expect("build request"); - let my_response = net - .response() - .status(200) - .body("Get OK") - .expect("request body"); + let request = client.get(url); + let my_response = net.response().status(200).body("Get OK"); net.on(request) - .respond(my_response) + .respond_with_body(my_response) .expect("on request, respond"); //when @@ -69,15 +61,11 @@ async fn test_post_url() { let client = net.client(); let url = "https://www.example.com"; - let request = client.post(url).build().expect("build request"); - let my_response = net - .response() - .status(200) - .body("Post OK") - .expect("request body"); + let request = client.post(url); + let my_response = net.response().status(200).body("Post OK"); net.on(request) - .respond(my_response) + .respond_with_body(my_response) .expect("on request, respond"); //when @@ -98,12 +86,8 @@ async fn test_post_by_method() { let client = net.client(); let url = "https://www.example.com"; - let request = client.post(url).build().expect("build request"); - let my_response = net - .response() - .status(200) - .body("Post OK") - .expect("request body"); + let request = client.post(url); + let my_response = net.response().status(200); net.on(request) .match_on(vec![ @@ -122,7 +106,7 @@ async fn test_post_by_method() { //then assert_eq!(response.status(), http::StatusCode::OK); - assert_eq!(response.bytes().await.expect("response body"), "Post OK"); + assert_eq!(response.bytes().await.expect("response body"), ""); } #[tokio::test] @@ -132,19 +116,15 @@ async fn test_post_by_url() { let client = net.client(); let url = "https://www.example.com"; - let request = client.post(url).build().expect("build request"); - let my_response = net - .response() - .status(200) - .body("Post OK") - .expect("request body"); + let request = client.post(url); + let my_response = net.response().status(200).body("Post OK"); net.on(request) .match_on(vec![ // MatchOn::Method, MatchOn::Url, ]) - .respond(my_response) + .respond_with_body(my_response) .expect("on request, respond"); //when @@ -166,16 +146,8 @@ async fn test_post_by_body() { let client = net.client(); let url = "https://www.example.com"; - let request = client - .post(url) - .body("match on body") - .build() - .expect("build request"); - let my_response = net - .response() - .status(200) - .body("response body") - .expect("request body"); + let request = client.post(url).body("match on body"); + let my_response = net.response().status(200).body("response body"); net.on(request) .match_on(vec![ @@ -183,7 +155,7 @@ async fn test_post_by_body() { // MatchOn::Url MatchOn::Body, ]) - .respond(my_response) + .respond_with_body(my_response) .expect("on request, respond"); //when @@ -208,17 +180,8 @@ async fn test_post_by_headers() { let client = net.client(); let url = "https://www.example.com"; - let request = client - .post(url) - .body("foo") - .header("test", "match") - .build() - .expect("build request"); - let my_response = net - .response() - .status(200) - .body("response body") - .expect("request body"); + let request = client.post(url).body("foo").header("test", "match"); + let my_response = net.response().status(200).body("response body"); net.on(request) .match_on(vec![ @@ -226,7 +189,7 @@ async fn test_post_by_headers() { // MatchOn::Url MatchOn::Headers, ]) - .respond(my_response) + .respond_with_body(my_response) .expect("on request, respond"); //when @@ -257,15 +220,11 @@ async fn test_unused_post() { let client = net.client(); let url = "https://www.example.com"; - let request = client.post(url).build().expect("build request"); - let my_response = net - .response() - .status(200) - .body("Post OK") - .expect("request body"); + let request = client.post(url); + let my_response = net.response().status(200).body("Post OK"); net.on(request) - .respond(my_response) + .respond_with_body(my_response) .expect("on request, respond"); //when