feat(net): cleaner mock.on syntax
OTHER
This commit is contained in:
parent
aad02be6cb
commit
87c67c97d0
5 changed files with 248 additions and 86 deletions
|
@ -114,14 +114,13 @@ mod tests {
|
||||||
let url = "http://localhost:8080";
|
let url = "http://localhost:8080";
|
||||||
|
|
||||||
// declare what response should be made for a given request
|
// declare what response should be made for a given request
|
||||||
let response: http::Response<&str> =
|
let response = mock_net.response().body("contents");
|
||||||
mock_net.response().body("contents").expect("response body");
|
let request = mock_net.client().get(url);
|
||||||
let request = mock_net.client().get(url).build().expect("request");
|
|
||||||
mock_net
|
mock_net
|
||||||
.on(request)
|
.on(request)
|
||||||
// By default, the METHOD and URL must match, equivalent to:
|
// By default, the METHOD and URL must match, equivalent to:
|
||||||
//.match_on(vec![MatchOn::Method, MatchOn::Url])
|
//.match_on(vec![MatchOn::Method, MatchOn::Url])
|
||||||
.respond(response)
|
.respond_with_body(response)
|
||||||
.expect("mock");
|
.expect("mock");
|
||||||
|
|
||||||
// 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
|
||||||
|
|
6
justfile
6
justfile
|
@ -10,6 +10,12 @@ build:
|
||||||
cargo test --example get
|
cargo test --example get
|
||||||
cargo mutants --jobs 4
|
cargo mutants --jobs 4
|
||||||
|
|
||||||
|
doc-test:
|
||||||
|
cargo doc
|
||||||
|
cargo test
|
||||||
|
cargo test --example get
|
||||||
|
|
||||||
|
|
||||||
install-hooks:
|
install-hooks:
|
||||||
@echo "Installing git hooks"
|
@echo "Installing git hooks"
|
||||||
git config core.hooksPath .git-hooks
|
git config core.hooksPath .git-hooks
|
||||||
|
|
150
src/net/mod.rs
150
src/net/mod.rs
|
@ -1,13 +1,161 @@
|
||||||
//! Provides a generic interface for network operations.
|
//! 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<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(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 result;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
pub use result::{Error, Result};
|
pub use result::{Error, Result};
|
||||||
|
|
||||||
pub use system::{MatchOn, MockNet, Net};
|
pub use system::{MatchOn, MockNet, Net, OnRequest};
|
||||||
|
|
||||||
/// Creates a new `Net`.
|
/// Creates a new `Net`.
|
||||||
pub const fn new() -> Net {
|
pub const fn new() -> Net {
|
||||||
|
|
|
@ -32,6 +32,7 @@ struct Plan {
|
||||||
match_on: Vec<MatchOn>,
|
match_on: Vec<MatchOn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An abstraction for the network
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Net {
|
pub struct Net {
|
||||||
inner: InnerNet<Unmocked>,
|
inner: InnerNet<Unmocked>,
|
||||||
|
@ -68,6 +69,14 @@ impl Net {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl Default for Net {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: InnerNet::<Unmocked>::new(),
|
||||||
|
mock: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
impl TryFrom<Net> for MockNet {
|
impl TryFrom<Net> for MockNet {
|
||||||
type Error = super::Error;
|
type Error = super::Error;
|
||||||
|
|
||||||
|
@ -87,7 +96,7 @@ impl MockNet {
|
||||||
pub fn client(&self) -> Client {
|
pub fn client(&self) -> Client {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
pub fn on(&self, request: impl Into<reqwest::Request>) -> OnRequest {
|
pub fn on(&self, request: impl Into<reqwest::RequestBuilder>) -> OnRequest {
|
||||||
self.inner.on(request)
|
self.inner.on(request)
|
||||||
}
|
}
|
||||||
/// Creates a [http::response::Builder] to be extended and returned by a mocked network request.
|
/// Creates a [http::response::Builder] to be extended and returned by a mocked network request.
|
||||||
|
@ -190,11 +199,14 @@ impl InnerNet<Mocked> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on(&self, request: impl Into<reqwest::Request>) -> OnRequest {
|
pub fn on(&self, request_builder: impl Into<reqwest::RequestBuilder>) -> OnRequest {
|
||||||
OnRequest {
|
match request_builder.into().build() {
|
||||||
net: self,
|
Ok(request) => OnRequest::Valid {
|
||||||
request: request.into(),
|
net: self,
|
||||||
match_on: vec![MatchOn::Method, MatchOn::Url],
|
request,
|
||||||
|
match_on: vec![MatchOn::Method, MatchOn::Url],
|
||||||
|
},
|
||||||
|
Err(err) => OnRequest::Error(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,20 +240,58 @@ impl<T: NetType> Drop for InnerNet<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OnRequest<'net> {
|
pub enum OnRequest<'net> {
|
||||||
net: &'net InnerNet<Mocked>,
|
Valid {
|
||||||
request: reqwest::Request,
|
net: &'net InnerNet<Mocked>,
|
||||||
match_on: Vec<MatchOn>,
|
request: reqwest::Request,
|
||||||
|
match_on: Vec<MatchOn>,
|
||||||
|
},
|
||||||
|
Error(super::Error),
|
||||||
}
|
}
|
||||||
impl<'net> OnRequest<'net> {
|
impl<'net> OnRequest<'net> {
|
||||||
pub fn match_on(self, match_on: Vec<MatchOn>) -> Self {
|
pub fn match_on(self, match_on: Vec<MatchOn>) -> Self {
|
||||||
Self {
|
if let OnRequest::Valid {
|
||||||
net: self.net,
|
net,
|
||||||
request: self.request,
|
request,
|
||||||
match_on,
|
match_on: _,
|
||||||
|
} = self
|
||||||
|
{
|
||||||
|
Self::Valid {
|
||||||
|
net,
|
||||||
|
request,
|
||||||
|
match_on,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn respond(self, response: impl Into<reqwest::Response>) -> Result<()> {
|
pub fn respond(self, response: impl Into<http::response::Builder>) -> Result<()> {
|
||||||
self.net._on(self.request, response.into(), self.match_on)
|
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<T>(
|
||||||
|
self,
|
||||||
|
body_result: std::result::Result<http::Response<T>, http::Error>,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
T: Into<reqwest::Body>,
|
||||||
|
{
|
||||||
|
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()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
89
tests/net.rs
89
tests/net.rs
|
@ -9,15 +9,11 @@ async fn test_get_url() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client.get(url).build().expect("build request");
|
let request = client.get(url);
|
||||||
let my_response = net
|
let my_response = net.response().status(200).body("Get OK");
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Get OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.respond(my_response)
|
.respond_with_body(my_response)
|
||||||
.expect("on request, respond");
|
.expect("on request, respond");
|
||||||
|
|
||||||
//when
|
//when
|
||||||
|
@ -38,15 +34,11 @@ async fn test_get_wrong_url() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client.get(url).build().expect("build request");
|
let request = client.get(url);
|
||||||
let my_response = net
|
let my_response = net.response().status(200).body("Get OK");
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Get OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.respond(my_response)
|
.respond_with_body(my_response)
|
||||||
.expect("on request, respond");
|
.expect("on request, respond");
|
||||||
|
|
||||||
//when
|
//when
|
||||||
|
@ -69,15 +61,11 @@ async fn test_post_url() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client.post(url).build().expect("build request");
|
let request = client.post(url);
|
||||||
let my_response = net
|
let my_response = net.response().status(200).body("Post OK");
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.respond(my_response)
|
.respond_with_body(my_response)
|
||||||
.expect("on request, respond");
|
.expect("on request, respond");
|
||||||
|
|
||||||
//when
|
//when
|
||||||
|
@ -98,12 +86,8 @@ async fn test_post_by_method() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client.post(url).build().expect("build request");
|
let request = client.post(url);
|
||||||
let my_response = net
|
let my_response = net.response().status(200);
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.match_on(vec![
|
.match_on(vec![
|
||||||
|
@ -122,7 +106,7 @@ async fn test_post_by_method() {
|
||||||
|
|
||||||
//then
|
//then
|
||||||
assert_eq!(response.status(), http::StatusCode::OK);
|
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]
|
#[tokio::test]
|
||||||
|
@ -132,19 +116,15 @@ async fn test_post_by_url() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client.post(url).build().expect("build request");
|
let request = client.post(url);
|
||||||
let my_response = net
|
let my_response = net.response().status(200).body("Post OK");
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.match_on(vec![
|
.match_on(vec![
|
||||||
// MatchOn::Method,
|
// MatchOn::Method,
|
||||||
MatchOn::Url,
|
MatchOn::Url,
|
||||||
])
|
])
|
||||||
.respond(my_response)
|
.respond_with_body(my_response)
|
||||||
.expect("on request, respond");
|
.expect("on request, respond");
|
||||||
|
|
||||||
//when
|
//when
|
||||||
|
@ -166,16 +146,8 @@ async fn test_post_by_body() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client
|
let request = client.post(url).body("match on body");
|
||||||
.post(url)
|
let my_response = net.response().status(200).body("response body");
|
||||||
.body("match on body")
|
|
||||||
.build()
|
|
||||||
.expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("response body")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.match_on(vec![
|
.match_on(vec![
|
||||||
|
@ -183,7 +155,7 @@ async fn test_post_by_body() {
|
||||||
// MatchOn::Url
|
// MatchOn::Url
|
||||||
MatchOn::Body,
|
MatchOn::Body,
|
||||||
])
|
])
|
||||||
.respond(my_response)
|
.respond_with_body(my_response)
|
||||||
.expect("on request, respond");
|
.expect("on request, respond");
|
||||||
|
|
||||||
//when
|
//when
|
||||||
|
@ -208,17 +180,8 @@ async fn test_post_by_headers() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client
|
let request = client.post(url).body("foo").header("test", "match");
|
||||||
.post(url)
|
let my_response = net.response().status(200).body("response body");
|
||||||
.body("foo")
|
|
||||||
.header("test", "match")
|
|
||||||
.build()
|
|
||||||
.expect("build request");
|
|
||||||
let my_response = net
|
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("response body")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.match_on(vec![
|
.match_on(vec![
|
||||||
|
@ -226,7 +189,7 @@ async fn test_post_by_headers() {
|
||||||
// MatchOn::Url
|
// MatchOn::Url
|
||||||
MatchOn::Headers,
|
MatchOn::Headers,
|
||||||
])
|
])
|
||||||
.respond(my_response)
|
.respond_with_body(my_response)
|
||||||
.expect("on request, respond");
|
.expect("on request, respond");
|
||||||
|
|
||||||
//when
|
//when
|
||||||
|
@ -257,15 +220,11 @@ async fn test_unused_post() {
|
||||||
let client = net.client();
|
let client = net.client();
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
let url = "https://www.example.com";
|
||||||
let request = client.post(url).build().expect("build request");
|
let request = client.post(url);
|
||||||
let my_response = net
|
let my_response = net.response().status(200).body("Post OK");
|
||||||
.response()
|
|
||||||
.status(200)
|
|
||||||
.body("Post OK")
|
|
||||||
.expect("request body");
|
|
||||||
|
|
||||||
net.on(request)
|
net.on(request)
|
||||||
.respond(my_response)
|
.respond_with_body(my_response)
|
||||||
.expect("on request, respond");
|
.expect("on request, respond");
|
||||||
|
|
||||||
//when
|
//when
|
||||||
|
|
Loading…
Reference in a new issue