diff --git a/example-app/Cargo.toml b/example-app/Cargo.toml index a183bcb..969c48e 100644 --- a/example-app/Cargo.toml +++ b/example-app/Cargo.toml @@ -8,6 +8,7 @@ tulsa = { path = "../tulsa" } axum = "0.6.18" hyper = { version = "0.14.27", features = ["client"] } prost = "0.12" +rand = "0.8.5" reqwest = { version = "0.11.18", features = ["json", "blocking"] } serde = { version = "1.0.171", features = ["derive"] } serde_json = "1.0.103" @@ -15,12 +16,10 @@ tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tower = "0.4.13" ureq = "2.7.1" mime = { version = "0.3.17", optional = true } +mockito = { version = "1.1.0", optional = true } [build-dependencies] prost-build = "0.12" -[dev-dependencies] -mockito = "1.1.0" - [features] -use_dependencies = ["mime"] +use_dependencies = [ "mime", "mockito" ] diff --git a/example-app/src/deps/mockito/error.rs b/example-app/src/deps/mockito/error.rs new file mode 100644 index 0000000..1a7048f --- /dev/null +++ b/example-app/src/deps/mockito/error.rs @@ -0,0 +1,15 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub struct MockError { + message: String, +} + +impl fmt::Display for MockError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MockError: {}", self.message) + } +} + +impl Error for MockError {} diff --git a/example-app/src/deps/mockito/mock.rs b/example-app/src/deps/mockito/mock.rs new file mode 100644 index 0000000..3b64945 --- /dev/null +++ b/example-app/src/deps/mockito/mock.rs @@ -0,0 +1,91 @@ +use hyper::StatusCode; +use hyper::{Body, Request}; +use rand; +use std::sync::{Arc, RwLock}; + +use super::state::State; + +#[derive(Clone)] +pub struct Response { + pub status: StatusCode, + pub body: Vec, +} + +impl Default for Response { + fn default() -> Self { + Response { + status: StatusCode::OK, + body: vec![], + } + } +} + +#[derive(Clone)] +pub struct InnerMock { + pub id: usize, + pub method: String, + pub path: String, + pub response: Response, + pub num_called: usize, +} + +#[derive(Clone)] +pub struct Mock { + pub state: Arc>, + pub inner: InnerMock, +} + +impl Mock { + pub fn new(state: Arc>, method: &str, path: &str) -> Mock { + Mock { + state, + inner: InnerMock { + id: rand::random(), + method: method.to_owned().to_uppercase(), + path: path.to_owned(), + response: Response::default(), + num_called: 0, + }, + } + } + + pub fn with_status(mut self, status: u16) -> Mock { + self.inner.response.status = StatusCode::from_u16(status).unwrap(); + self + } + + pub fn with_body(mut self, body: Vec) -> Mock { + self.inner.response.body = body; + self + } + + pub fn create(self) -> Mock { + let state = self.state.clone(); + let mut state = state.write().unwrap(); + state.mocks.push(self.clone()); + self + } + + pub fn assert(&self) { + let state = self.state.clone(); + let state = state.read().unwrap(); + let num_called = state + .mocks + .iter() + .find(|mock| mock.inner.id == self.inner.id) + .unwrap() + .inner + .num_called; + + if num_called == 0 { + panic!("Mock not called"); + } + } + + pub fn matches(&self, request: &Request) -> bool { + let method = request.method().to_string(); + let path = request.uri().path().to_string(); + + method == self.inner.method && path == self.inner.path + } +} diff --git a/example-app/src/deps/mockito/mod.rs b/example-app/src/deps/mockito/mod.rs new file mode 100644 index 0000000..8ec6e9c --- /dev/null +++ b/example-app/src/deps/mockito/mod.rs @@ -0,0 +1,6 @@ +mod error; +mod mock; +mod server; +mod state; + +pub use server::Server; diff --git a/example-app/src/deps/mockito/server.rs b/example-app/src/deps/mockito/server.rs new file mode 100644 index 0000000..6304245 --- /dev/null +++ b/example-app/src/deps/mockito/server.rs @@ -0,0 +1,85 @@ +use hyper::server::conn::Http; +use hyper::service::service_fn; +use hyper::{Body, Request, Response as HyperResponse}; +use std::net::SocketAddr; +use std::sync::{Arc, RwLock}; +use std::thread; +use tokio::net::TcpListener; +use tokio::runtime; +use tokio::task::spawn; + +use super::error::MockError; +use super::mock::Mock; +use super::state::State; + +pub struct Server { + address: SocketAddr, + state: Arc>, +} + +impl Server { + pub fn new() -> Server { + let address = SocketAddr::from(([127, 0, 0, 1], 5001)); + let state = Arc::new(RwLock::new(State::new())); + + let runtime = runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let state_b = state.clone(); + thread::spawn(move || { + runtime.block_on(async { + let listener = TcpListener::bind(address).await.unwrap(); + + while let Ok((stream, _)) = listener.accept().await { + let state_c = state_b.clone(); + spawn(async move { + let _ = Http::new() + .serve_connection( + stream, + service_fn(move |request: Request| { + handle_request(request, state_c.clone()) + }), + ) + .await; + }); + } + }); + }); + + Server { address, state } + } + + pub fn mock(&self, method: &str, path: &str) -> Mock { + Mock::new(self.state.clone(), method, path) + } + + pub fn url(&self) -> String { + format!("http://{}", self.address.to_string()) + } +} + +async fn handle_request( + request: Request, + state: Arc>, +) -> Result, MockError> { + let state_b = state.clone(); + let mut state = state_b.write().unwrap(); + let mut matching: Vec<&mut Mock> = vec![]; + + for mock in state.mocks.iter_mut() { + if mock.matches(&request) { + matching.push(mock); + } + } + let mock = matching.first_mut(); + + if let Some(mock) = mock { + mock.inner.num_called += 1; + let response = HyperResponse::new(Body::from(mock.inner.response.body.clone())); + Ok(response) + } else { + panic!("No matching mock found"); + } +} diff --git a/example-app/src/deps/mockito/state.rs b/example-app/src/deps/mockito/state.rs new file mode 100644 index 0000000..e54c00f --- /dev/null +++ b/example-app/src/deps/mockito/state.rs @@ -0,0 +1,11 @@ +use super::mock::Mock; + +pub struct State { + pub mocks: Vec, +} + +impl State { + pub fn new() -> Self { + State { mocks: vec![] } + } +} diff --git a/example-app/src/fetcher.rs b/example-app/src/fetcher.rs index d97eeb6..d94a74e 100644 --- a/example-app/src/fetcher.rs +++ b/example-app/src/fetcher.rs @@ -120,6 +120,9 @@ pub async fn recurring_fetch(feed: Feed) { #[cfg(test)] mod tests { + #[cfg(not(feature = "use_dependencies"))] + use crate::deps::mockito; + use super::*; use std::collections::HashMap; use std::fs; diff --git a/example-app/src/lib.rs b/example-app/src/lib.rs index 05c4ff6..825fa29 100644 --- a/example-app/src/lib.rs +++ b/example-app/src/lib.rs @@ -6,7 +6,10 @@ pub mod scheduler_interface; // a learning exercise. I do not plan to make this code public and my // implementations will definitely be similar to the original since I am not // yet fluent in Rust. +#[cfg(not(feature = "use_dependencies"))] pub mod deps { // mime = "0.3.17" -> https://docs.rs/mime/0.3.17/mime/ pub mod mime; + // mockito = "1.1.0" -> https://docs.rs/mockito/1.1.0/mockito/ + pub mod mockito; }