Compare commits

..

No commits in common. "c80f8036ec0c7b8b5fb19567a12e45b438f8b62b" and "e053679dcc9f469f0e6a2d5da577c618588d57db" have entirely different histories.

6 changed files with 173 additions and 50 deletions

View file

@ -21,7 +21,7 @@ derive_more = { version = "1.0", features = [
] } ] }
http = "1.1" http = "1.1"
path-clean = "1.0" path-clean = "1.0"
reqwest = { version = "0.12", features = [ "json" ] } reqwest = "0.12"
url = "2.5" url = "2.5"
tempfile = "3.10" tempfile = "3.10"

View file

@ -62,8 +62,7 @@ async fn download_and_save_to_file(
println!("fetching: {url}"); println!("fetching: {url}");
// Uses the network abstraction to create a perfectly normal `reqwest::ResponseBuilder`. // Uses the network abstraction to create a perfectly normal `reqwest::ResponseBuilder`.
// `kxio::net::RequestBuilder` is an alias. let request: reqwest::RequestBuilder = net.client().get(url);
let request: kxio::net::RequestBuilder = net.client().get(url);
// Rather than calling `.build().send()?` on the request, pass it to the `net` // Rather than calling `.build().send()?` on the request, pass it to the `net`
// This allows the `net` to either make the network request as normal, or, if we are // This allows the `net` to either make the network request as normal, or, if we are
@ -71,8 +70,7 @@ async fn download_and_save_to_file(
// NOTE: if the `.build().send()` is called on the `request` then that WILL result in // NOTE: if the `.build().send()` is called on the `request` then that WILL result in
// a real network request being made, even under test conditions. Only ever use the // a real network request being made, even under test conditions. Only ever use the
// `net.send(...)` function to keep your code testable. // `net.send(...)` function to keep your code testable.
// `kxio::net::Response` is an alias for `reqwest::Response`. let response: reqwest::Response = net.send(request).await?;
let response: kxio::net::Response = net.send(request).await?;
let body = response.text().await?; let body = response.text().await?;
println!("fetched {} bytes", body.bytes().len()); println!("fetched {} bytes", body.bytes().len());
@ -104,8 +102,6 @@ fn delete_file(file_path: &Path, fs: &kxio::fs::FileSystem) -> kxio::Result<()>
mod tests { mod tests {
use super::*; use super::*;
use kxio::net::{Method, Url};
// This test demonstrates how to use the `kxio` to test your program. // This test demonstrates how to use the `kxio` to test your program.
#[tokio::test] #[tokio::test]
async fn should_save_remote_body() { async fn should_save_remote_body() {
@ -120,8 +116,8 @@ mod tests {
// declare what response should be made for a given request // declare what response should be made for a given request
let response = mock_net.response().body("contents").expect("response body"); let response = mock_net.response().body("contents").expect("response body");
mock_net mock_net
.on(Method::GET) .on(http::Method::GET)
.url(Url::parse(url).expect("parse url")) .url(url::Url::parse(url).expect("parse url"))
.respond(response); .respond(response);
// Create a temporary directory that will be deleted with `fs` goes out of scope // Create a temporary directory that will be deleted with `fs` goes out of scope

View file

@ -21,7 +21,7 @@
//! //!
//! Write your program to take a reference to [Net]. //! Write your program to take a reference to [Net].
//! //!
//! Use the [Net::client] functionto create a [RequestBuilder] which you should then pass to the [Net::send] method. //! 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. //! 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 //! ```rust
@ -62,14 +62,12 @@
//! let mock_net = net::mock(); //! let mock_net = net::mock();
//! ``` //! ```
//! //!
//! Create a [Client] using [MockNet::client()]. //! Create a [reqwest::Client] using [MockNet::client()].
//! //!
//! ```rust //! ```rust
//! # let mock_net = kxio::net::mock(); //! # let mock_net = kxio::net::mock();
//! let client = mock_net.client(); //! let client = mock_net.client();
//! // this is the same as: //! // this is the same as:
//! let client = kxio::net::Client::new();
//! // this is also the same as:
//! let client = reqwest::Client::new(); //! let client = reqwest::Client::new();
//! ``` //! ```
//! //!
@ -79,16 +77,16 @@
//! //!
//! ```rust //! ```rust
//! use kxio::net; //! use kxio::net;
//! use kxio::net::{Method, Url}; //! use kxio::net::MatchOn;
//! //!
//! # #[tokio::main] //! # #[tokio::main]
//! # async fn main() -> net::Result<()> { //! # async fn main() -> net::Result<()> {
//! # let mock_net = net::mock(); //! # let mock_net = net::mock();
//! mock_net.on(Method::GET) //! mock_net.on(http::Method::GET)
//! .url(Url::parse("https://example.com")?) //! .url(url::Url::parse("https://example.com")?)
//! .respond(mock_net.response().status(200).body("")?); //! .respond(mock_net.response().status(200).body("")?);
//! mock_net.on(Method::GET) //! mock_net.on(http::Method::GET)
//! .url(Url::parse("https://example.com/foo")?) //! .url(url::Url::parse("https://example.com/foo")?)
//! .respond(mock_net.response().status(500).body("Mocked response")?); //! .respond(mock_net.response().status(500).body("Mocked response")?);
//! # mock_net.reset(); //! # mock_net.reset();
//! # Ok(()) //! # Ok(())
@ -145,6 +143,144 @@
//! The module uses a custom [Result] type that wraps `core::result::Result` with the custom [Error] enum, //! 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. //! allowing for specific error handling related to network operations.
//! Provides a testable interface over the [reqwest] crate. //! 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<T, Error>`
//! - [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(http::Method::GET)
//! .url(url::Url::parse("https://example.com")?)
//! .respond(mock_net.response().status(200).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 result;
mod system; mod system;
@ -153,13 +289,6 @@ pub use result::{Error, Result};
pub use system::{MatchOn, MockNet, Net}; pub use system::{MatchOn, MockNet, Net};
pub use http::Method;
pub use reqwest::Client;
pub use reqwest::Request;
pub use reqwest::RequestBuilder;
pub use reqwest::Response;
pub use url::Url;
/// Creates a new `Net`. /// Creates a new `Net`.
pub const fn new() -> Net { pub const fn new() -> Net {
Net::new() Net::new()

View file

@ -1,8 +1,6 @@
// //
use derive_more::derive::From; use derive_more::derive::From;
use crate::net::Request;
/// The Errors that may occur within [kxio::net][crate::net]. /// The Errors that may occur within [kxio::net][crate::net].
#[derive(Debug, From, derive_more::Display)] #[derive(Debug, From, derive_more::Display)]
pub enum Error { pub enum Error {
@ -22,7 +20,7 @@ pub enum Error {
/// There was network request that doesn't match any that were expected /// There was network request that doesn't match any that were expected
#[display("Unexpected request: {0}", 0.to_string())] #[display("Unexpected request: {0}", 0.to_string())]
UnexpectedMockRequest(Request), UnexpectedMockRequest(reqwest::Request),
/// There was an error accessing the list of expected requests. /// There was an error accessing the list of expected requests.
RwLockLocked, RwLockLocked,

View file

@ -3,8 +3,6 @@ use std::cell::RefCell;
use reqwest::{Body, Client}; use reqwest::{Body, Client};
use crate::net::{Method, Request, RequestBuilder, Response, Url};
use super::{Error, Result}; use super::{Error, Result};
/// A list of planned requests and responses /// A list of planned requests and responses
@ -31,10 +29,10 @@ pub enum MatchOn {
/// Contains a list of the criteria that a request must meet before being considered a match. /// Contains a list of the criteria that a request must meet before being considered a match.
struct Plan { struct Plan {
match_request: Vec<MatchRequest>, match_request: Vec<MatchRequest>,
response: Response, response: reqwest::Response,
} }
impl Plan { impl Plan {
fn matches(&self, request: &Request) -> bool { fn matches(&self, request: &reqwest::Request) -> bool {
self.match_request.iter().all(|criteria| match criteria { self.match_request.iter().all(|criteria| match criteria {
MatchRequest::Method(method) => request.method() == method, MatchRequest::Method(method) => request.method() == method,
MatchRequest::Url(uri) => request.url() == uri, MatchRequest::Url(uri) => request.url() == uri,
@ -74,7 +72,7 @@ impl Net {
} }
} }
impl Net { impl Net {
/// Helper to create a default [Client]. /// Helper to create a default [reqwest::Client].
/// ///
/// # Example /// # Example
/// ///
@ -84,7 +82,7 @@ impl Net {
/// let client = net.client(); /// let client = net.client();
/// let request = client.get("https://hyper.rs"); /// let request = client.get("https://hyper.rs");
/// ``` /// ```
pub fn client(&self) -> Client { pub fn client(&self) -> reqwest::Client {
Default::default() Default::default()
} }
@ -107,7 +105,10 @@ impl Net {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub async fn send(&self, request: impl Into<RequestBuilder>) -> Result<Response> { pub async fn send(
&self,
request: impl Into<reqwest::RequestBuilder>,
) -> Result<reqwest::Response> {
let Some(plans) = &self.plans else { let Some(plans) = &self.plans else {
return request.into().send().await.map_err(Error::from); return request.into().send().await.map_err(Error::from);
}; };
@ -147,14 +148,13 @@ impl TryFrom<Net> for MockNet {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// use kxio::net::{Method, Url};
/// # use kxio::net::Result; /// # use kxio::net::Result;
/// # fn run() -> Result<()> { /// # fn run() -> Result<()> {
/// let mock_net = kxio::net::mock(); /// let mock_net = kxio::net::mock();
/// let client = mock_net.client(); /// let client = mock_net.client();
/// // define an expected requet, and the response that should be returned /// // define an expected requet, and the response that should be returned
/// mock_net.on(Method::GET) /// mock_net.on(http::Method::GET)
/// .url(Url::parse("https://hyper.rs")?) /// .url(url::Url::parse("https://hyper.rs")?)
/// .respond(mock_net.response().status(200).body("Ok")?); /// .respond(mock_net.response().status(200).body("Ok")?);
/// let net: kxio::net::Net = mock_net.into(); /// let net: kxio::net::Net = mock_net.into();
/// // use 'net' in your program, by passing it as a reference /// // use 'net' in your program, by passing it as a reference
@ -170,7 +170,7 @@ pub struct MockNet {
plans: RefCell<Plans>, plans: RefCell<Plans>,
} }
impl MockNet { impl MockNet {
/// Helper to create a default [Client]. /// Helper to create a default [reqwest::Client].
/// ///
/// # Example /// # Example
/// ///
@ -188,18 +188,17 @@ impl MockNet {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// use kxio::net::{Method, Url};
/// # use kxio::net::Result; /// # use kxio::net::Result;
/// # fn run() -> Result<()> { /// # fn run() -> Result<()> {
/// let mock_net = kxio::net::mock(); /// let mock_net = kxio::net::mock();
/// let client = mock_net.client(); /// let client = mock_net.client();
/// mock_net.on(Method::GET) /// mock_net.on(http::Method::GET)
/// .url(Url::parse("https://hyper.rs")?) /// .url(url::Url::parse("https://hyper.rs")?)
/// .respond(mock_net.response().status(200).body("Ok")?); /// .respond(mock_net.response().status(200).body("Ok")?);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn on(&self, method: impl Into<Method>) -> WhenRequest { pub fn on(&self, method: impl Into<http::Method>) -> WhenRequest {
WhenRequest::new(self, method) WhenRequest::new(self, method)
} }
@ -257,8 +256,8 @@ impl Drop for Net {
} }
pub enum MatchRequest { pub enum MatchRequest {
Method(Method), Method(http::Method),
Url(Url), Url(reqwest::Url),
Header { name: String, value: String }, Header { name: String, value: String },
Body(String), Body(String),
} }
@ -269,7 +268,7 @@ pub struct WhenRequest<'net> {
} }
impl<'net> WhenRequest<'net> { impl<'net> WhenRequest<'net> {
pub fn url(mut self, url: impl Into<Url>) -> Self { pub fn url(mut self, url: impl Into<reqwest::Url>) -> Self {
self.match_on.push(MatchRequest::Url(url.into())); self.match_on.push(MatchRequest::Url(url.into()));
self self
} }
@ -294,7 +293,7 @@ impl<'net> WhenRequest<'net> {
}); });
} }
fn new(net: &'net MockNet, method: impl Into<Method>) -> Self { fn new(net: &'net MockNet, method: impl Into<http::Method>) -> Self {
Self { Self {
net, net,
match_on: vec![MatchRequest::Method(method.into())], match_on: vec![MatchRequest::Method(method.into())],

View file

@ -1,7 +1,8 @@
//
use kxio::net::{Error, Method, MockNet, Net, Url};
use assert2::let_assert; use assert2::let_assert;
use http::Method;
//
use kxio::net::{Error, MockNet, Net};
use reqwest::Url;
#[tokio::test] #[tokio::test]
async fn test_get_url() { async fn test_get_url() {