feat: Net<Mocked> uses internal mutability
All checks were successful
Rust / build (map[name:stable]) (push) Successful in 1m24s
Rust / build (map[name:nightly]) (push) Successful in 3m52s
Release Please / Release-plz (push) Successful in 1m36s

This commit is contained in:
Paul Campbell 2024-11-09 12:13:06 +00:00
parent 17c1b4ff6d
commit 69c1ac8565
3 changed files with 64 additions and 39 deletions

View file

@ -1,5 +1,4 @@
// //
use derive_more::derive::From; use derive_more::derive::From;
/// Represents a error accessing the network. /// Represents a error accessing the network.
@ -9,14 +8,14 @@ pub enum Error {
Request(String), Request(String),
#[display("Unexpected request: {0}", 0.to_string())] #[display("Unexpected request: {0}", 0.to_string())]
UnexpectedMockRequest(reqwest::Request), UnexpectedMockRequest(reqwest::Request),
RwLockLocked,
} }
impl std::error::Error for Error {} impl std::error::Error for Error {}
impl Clone for Error { impl Clone for Error {
fn clone(&self) -> Self { fn clone(&self) -> Self {
match self { match self {
Self::Reqwest(req) => Self::Request(req.to_string()), Self::Reqwest(req) => Self::Request(req.to_string()),
Self::Request(req) => Self::Request(req.clone()), err => err.clone(),
Self::UnexpectedMockRequest(_) => todo!(),
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
use std::marker::PhantomData; use std::{marker::PhantomData, sync::RwLock};
use super::{Error, Result}; use super::{Error, Result};
@ -27,19 +27,20 @@ pub struct Plan {
match_on: Vec<MatchOn>, match_on: Vec<MatchOn>,
} }
#[derive(Debug)]
pub struct Net<T: NetType> { pub struct Net<T: NetType> {
_type: PhantomData<T>, _type: PhantomData<T>,
plans: Plans, plans: RwLock<Plans>,
} }
impl Net<Unmocked> { impl Net<Unmocked> {
pub(crate) const fn new() -> Self { pub(crate) const fn new() -> Self {
Self { Self {
_type: PhantomData, _type: PhantomData,
plans: vec![], plans: RwLock::new(vec![]),
} }
} }
pub async fn send(&mut self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> { pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
request.send().await.map_err(Error::from) request.send().await.map_err(Error::from)
} }
} }
@ -53,12 +54,14 @@ impl Net<Mocked> {
pub(crate) const fn new() -> Self { pub(crate) const fn new() -> Self {
Self { Self {
_type: PhantomData, _type: PhantomData,
plans: vec![], plans: RwLock::new(vec![]),
} }
} }
pub async fn send(&mut self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
let request = request.build()?; let request = request.build()?;
let index = self.plans.iter().position(|plan| { let read_plans = self.plans.read().map_err(|_| Error::RwLockLocked)?;
let index = read_plans.iter().position(|plan| {
// METHOD // METHOD
(if plan.match_on.contains(&MatchOn::Method) { (if plan.match_on.contains(&MatchOn::Method) {
plan.request.method() == request.method() plan.request.method() == request.method()
@ -88,18 +91,22 @@ impl Net<Mocked> {
true true
}) })
}); });
drop(read_plans);
match index { match index {
Some(i) => Ok(self.plans.remove(i).response), Some(i) => {
let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?;
Ok(write_plans.remove(i).response)
}
None => Err(Error::UnexpectedMockRequest(request)), None => Err(Error::UnexpectedMockRequest(request)),
} }
} }
/// Creates a [ResponseBuilder] 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.
pub fn response(&self) -> http::response::Builder { pub fn response(&self) -> http::response::Builder {
Default::default() Default::default()
} }
pub fn on(&mut self, request: reqwest::Request) -> OnRequest { pub fn on(&self, request: reqwest::Request) -> OnRequest {
OnRequest { OnRequest {
net: self, net: self,
request, request,
@ -108,30 +115,37 @@ impl Net<Mocked> {
} }
fn _on( fn _on(
&mut self, &self,
request: reqwest::Request, request: reqwest::Request,
response: reqwest::Response, response: reqwest::Response,
match_on: Vec<MatchOn>, match_on: Vec<MatchOn>,
) { ) -> Result<()> {
self.plans.push(Plan { let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?;
write_plans.push(Plan {
request, request,
response, response,
match_on, match_on,
}) });
Ok(())
} }
pub fn reset(&mut self) { pub fn reset(&self) -> Result<()> {
self.plans = vec![]; let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?;
write_plans.clear();
Ok(())
} }
} }
impl<T: NetType> Drop for Net<T> { impl<T: NetType> Drop for Net<T> {
fn drop(&mut self) { fn drop(&mut self) {
assert!(self.plans.is_empty()) let Ok(read_plans) = self.plans.read() else {
return;
};
assert!(read_plans.is_empty())
} }
} }
pub struct OnRequest<'net> { pub struct OnRequest<'net> {
net: &'net mut Net<Mocked>, net: &'net Net<Mocked>,
request: reqwest::Request, request: reqwest::Request,
match_on: Vec<MatchOn>, match_on: Vec<MatchOn>,
} }
@ -143,7 +157,7 @@ impl<'net> OnRequest<'net> {
match_on, match_on,
} }
} }
pub fn respond(self, response: reqwest::Response) { pub fn respond(self, response: reqwest::Response) -> Result<()> {
self.net._on(self.request, response, self.match_on) self.net._on(self.request, response, self.match_on)
} }
} }

View file

@ -5,7 +5,7 @@ use kxio::net::{Error, MatchOn};
#[tokio::test] #[tokio::test]
async fn test_get_url() { async fn test_get_url() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -16,7 +16,9 @@ async fn test_get_url() {
.body("Get OK") .body("Get OK")
.expect("request body"); .expect("request body");
net.on(request).respond(my_response.into()); net.on(request)
.respond(my_response.into())
.expect("on request, respond");
//when //when
let response = net.send(client.get(url)).await.expect("response"); let response = net.send(client.get(url)).await.expect("response");
@ -29,7 +31,7 @@ async fn test_get_url() {
#[tokio::test] #[tokio::test]
async fn test_get_wrong_url() { async fn test_get_wrong_url() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -40,7 +42,9 @@ async fn test_get_wrong_url() {
.body("Get OK") .body("Get OK")
.expect("request body"); .expect("request body");
net.on(request).respond(my_response.into()); net.on(request)
.respond(my_response.into())
.expect("on request, respond");
//when //when
let_assert!( let_assert!(
@ -52,13 +56,13 @@ async fn test_get_wrong_url() {
assert_eq!(invalid_request.url().to_string(), "https://some.other.url/"); assert_eq!(invalid_request.url().to_string(), "https://some.other.url/");
// remove pending unmatched request - we never meant to match against it // remove pending unmatched request - we never meant to match against it
net.reset(); net.reset().expect("reset");
} }
#[tokio::test] #[tokio::test]
async fn test_post_url() { async fn test_post_url() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -69,7 +73,9 @@ async fn test_post_url() {
.body("Post OK") .body("Post OK")
.expect("request body"); .expect("request body");
net.on(request).respond(my_response.into()); net.on(request)
.respond(my_response.into())
.expect("on request, respond");
//when //when
let response = net.send(client.post(url)).await.expect("reponse"); let response = net.send(client.post(url)).await.expect("reponse");
@ -82,7 +88,7 @@ async fn test_post_url() {
#[tokio::test] #[tokio::test]
async fn test_post_by_method() { async fn test_post_by_method() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -98,7 +104,8 @@ async fn test_post_by_method() {
MatchOn::Method, MatchOn::Method,
// MatchOn::Url // MatchOn::Url
]) ])
.respond(my_response.into()); .respond(my_response.into())
.expect("on request, respond");
//when //when
// This request is a different url - but should still match // This request is a different url - but should still match
@ -115,7 +122,7 @@ async fn test_post_by_method() {
#[tokio::test] #[tokio::test]
async fn test_post_by_url() { async fn test_post_by_url() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -131,7 +138,8 @@ async fn test_post_by_url() {
// MatchOn::Method, // MatchOn::Method,
MatchOn::Url, MatchOn::Url,
]) ])
.respond(my_response.into()); .respond(my_response.into())
.expect("on request, respond");
//when //when
// This request is a GET, not POST - but should still match // This request is a GET, not POST - but should still match
@ -145,7 +153,7 @@ async fn test_post_by_url() {
#[tokio::test] #[tokio::test]
async fn test_post_by_body() { async fn test_post_by_body() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -166,7 +174,8 @@ async fn test_post_by_body() {
// MatchOn::Url // MatchOn::Url
MatchOn::Body, MatchOn::Body,
]) ])
.respond(my_response.into()); .respond(my_response.into())
.expect("on request, respond");
//when //when
// This request is a GET, not POST - but should still match // This request is a GET, not POST - but should still match
@ -186,7 +195,7 @@ async fn test_post_by_body() {
#[tokio::test] #[tokio::test]
async fn test_post_by_headers() { async fn test_post_by_headers() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -208,7 +217,8 @@ async fn test_post_by_headers() {
// MatchOn::Url // MatchOn::Url
MatchOn::Headers, MatchOn::Headers,
]) ])
.respond(my_response.into()); .respond(my_response.into())
.expect("on request, respond");
//when //when
// This request is a GET, not POST - but should still match // This request is a GET, not POST - but should still match
@ -234,7 +244,7 @@ async fn test_post_by_headers() {
#[should_panic] #[should_panic]
async fn test_unused_post() { async fn test_unused_post() {
//given //given
let mut net = kxio::net::mock(); let net = kxio::net::mock();
let client = net.client(); let client = net.client();
let url = "https://www.example.com"; let url = "https://www.example.com";
@ -245,7 +255,9 @@ async fn test_unused_post() {
.body("Post OK") .body("Post OK")
.expect("request body"); .expect("request body");
net.on(request).respond(my_response.into()); net.on(request)
.respond(my_response.into())
.expect("on request, respond");
//when //when
// don't send the planned request // don't send the planned request