From f6b35d05e775f4121f8432c314de01ba31db2e5e Mon Sep 17 00:00:00 2001 From: Hamcha <hamcha@crunchy.rocks> Date: Sun, 26 Jan 2025 13:25:24 +0100 Subject: [PATCH] testing the components??? --- Cargo.lock | 77 ++++++++++++++ Cargo.toml | 3 +- src/inbound/renderer/mod.rs | 3 + src/inbound/renderer/page.rs | 118 +++++++++++++++++++++ src/inbound/renderer/server.rs | 17 +-- src/inbound/renderer/testing/mod.rs | 7 ++ src/main.rs | 4 +- src/outbound/repository/adapters/memory.rs | 35 +++++- src/outbound/repository/site.rs | 7 +- src/outbound/services/site.rs | 107 ++++++++++++++----- 10 files changed, 338 insertions(+), 40 deletions(-) create mode 100644 src/inbound/renderer/testing/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f824cef..f2c382b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + [[package]] name = "askama_escape" version = "0.10.3" @@ -869,6 +875,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -967,6 +979,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures" version = "0.3.31" @@ -1540,6 +1558,7 @@ dependencies = [ "dioxus-cli-config", "dioxus-logger", "dotenvy", + "mockall", "serde", "thiserror 2.0.11", "tokio", @@ -1628,6 +1647,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "multer" version = "3.1.0" @@ -1806,6 +1851,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -2319,6 +2390,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index bea5917..3d870f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,14 @@ dioxus = { version = "0.6", features = ["fullstack", "router"] } dioxus-cli-config = "0.6" dioxus-logger = "0.6" dotenvy = { version = "0.15", optional = true } +mockall = { version = "0.13", optional = true } serde = { version = "1", features = ["derive"] } thiserror = "2" tokio = { version = "1", features = ["full"], optional = true } [features] default = [] -server = ["dioxus/server", "tokio", "dotenvy"] +server = ["dioxus/server", "tokio", "dotenvy", "mockall"] web = ["dioxus/web"] tokio = ["dep:tokio"] diff --git a/src/inbound/renderer/mod.rs b/src/inbound/renderer/mod.rs index ada7c5e..4e3213e 100644 --- a/src/inbound/renderer/mod.rs +++ b/src/inbound/renderer/mod.rs @@ -11,6 +11,9 @@ mod meta; mod page; mod server; +#[cfg(test)] +mod testing; + #[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[rustfmt::skip] enum Route { diff --git a/src/inbound/renderer/page.rs b/src/inbound/renderer/page.rs index 4122d52..0148041 100644 --- a/src/inbound/renderer/page.rs +++ b/src/inbound/renderer/page.rs @@ -71,3 +71,121 @@ pub fn BlockElement(block: Block) -> Element { }, } } + +#[cfg(test)] +mod tests { + use meta::SiteContext; + + use crate::{ + domain::entities::{ + cursor::Paginated, + site::{Image, SiteInfo}, + }, + outbound::services::site::{MockSiteService, SiteServiceProvider}, + }; + + use super::*; + + #[test] + fn block_renders_text() { + let text = "Hello, world!"; + let block = Block::Text { + text: text.to_string(), + }; + let element = rsx! { + BlockElement { block } + }; + let elem_str = dioxus::ssr::render_element(element); + assert!(elem_str.contains(text)); + } + + #[test] + fn block_gallery_renders_images() { + let images = vec![ + Image { + src: "https://example.com/image1.jpg".to_string(), + caption: "Image 1".to_string(), + }, + Image { + src: "https://example.com/image2.jpg".to_string(), + caption: "Image 2".to_string(), + }, + ]; + let block = Block::Gallery { images }; + let element = rsx! { + BlockElement { block } + }; + let elem_str = dioxus::ssr::render_element(element); + assert!(elem_str.contains("<img")); + assert!(elem_str.contains("https://example.com/image1.jpg")); + assert!(elem_str.contains("https://example.com/image2.jpg")); + } + + #[test] + fn post_renders_blocks() { + let blocks = vec![ + Block::Text { + text: "Hello, world!".to_string(), + }, + Block::Text { + text: "Something else!".to_string(), + }, + ]; + let post = Post { blocks }; + let element = rsx! { + PostElement { post } + }; + let elem_str = dioxus::ssr::render_element(element); + assert!(elem_str.contains("Hello, world!")); + assert!(elem_str.contains("Something else!")); + } + + #[test] + fn collection_renders_posts() { + let mut app = VirtualDom::new(|| { + rsx! { + Collection { collection_id: "".to_string() } + } + }); + + app.provide_root_context(SiteContext { + info: SiteInfo { + title: "test".to_string(), + domain: "test".to_string(), + pages: vec![], + }, + }); + + let mut mock_service = MockSiteService::new(); + mock_service + .expect_get_posts() + .times(1) + .returning(move |_, _, _| { + Box::pin(async { + Ok(Paginated { + data: vec![ + Post { + blocks: vec![Block::Text { + text: "Hello, world!".to_string(), + }], + }, + Post { + blocks: vec![Block::Text { + text: "Something else!".to_string(), + }], + }, + ], + next: None, + }) + }) + }); + + server_context().insert(SiteServiceProvider::with(mock_service)); + + app.rebuild_in_place(); + let elem_str = dioxus::ssr::render(&app); + println!("elem_str: {elem_str}"); + assert!(elem_str.contains("Hello, world!")); + assert!(elem_str.contains("Something else!")); + } +} diff --git a/src/inbound/renderer/server.rs b/src/inbound/renderer/server.rs index bd921c8..aa705e7 100644 --- a/src/inbound/renderer/server.rs +++ b/src/inbound/renderer/server.rs @@ -7,8 +7,8 @@ use crate::domain::entities::{ #[server] pub async fn get_site_info() -> Result<SiteInfo, ServerFnError> { - use crate::outbound::services::site::SiteService; - let FromContext::<SiteService>(service) = extract().await?; + use crate::outbound::services::site::SiteServiceProvider; + let FromContext(SiteServiceProvider { service }) = extract().await?; let headers = server_context().request_parts().headers.clone(); let domain = match headers.get("host").and_then(|h| h.to_str().ok()) { @@ -26,8 +26,8 @@ pub async fn get_site_info() -> Result<SiteInfo, ServerFnError> { #[server] pub async fn get_page(domain: String, page: String) -> Result<Page, ServerFnError> { - use crate::outbound::services::site::SiteService; - let FromContext::<SiteService>(service) = extract().await?; + use crate::outbound::services::site::SiteServiceProvider; + let FromContext(SiteServiceProvider { service }) = extract().await?; Ok(service.get_page(&domain, &page).await?) } @@ -37,8 +37,9 @@ pub async fn get_posts( domain: String, collection_id: String, ) -> Result<Paginated<Post, String>, ServerFnError> { - use crate::outbound::services::site::SiteService; - let FromContext::<SiteService>(service) = extract().await?; + println!("server_context: {:#?}", server_context().request_parts()); + use crate::outbound::services::site::SiteServiceProvider; + let FromContext(SiteServiceProvider { service }) = extract().await?; Ok(service.get_posts(&domain, &collection_id, None).await?) } @@ -49,8 +50,8 @@ pub async fn get_post( collection_id: String, id: String, ) -> Result<Post, ServerFnError> { - use crate::outbound::services::site::SiteService; - let FromContext::<SiteService>(service) = extract().await?; + use crate::outbound::services::site::SiteServiceProvider; + let FromContext(SiteServiceProvider { service }) = extract().await?; Ok(service.get_post(&domain, &collection_id, &id).await?) } diff --git a/src/inbound/renderer/testing/mod.rs b/src/inbound/renderer/testing/mod.rs new file mode 100644 index 0000000..8a52814 --- /dev/null +++ b/src/inbound/renderer/testing/mod.rs @@ -0,0 +1,7 @@ +use dioxus::prelude::*; + +#[component] +pub fn UseSiteContext() -> Element { + use_context_provider(|| Signal::new(0)); + rsx! {} +} diff --git a/src/main.rs b/src/main.rs index 57e951b..67eab0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,10 +15,10 @@ fn main() { let mut builder = dioxus::LaunchBuilder::new(); server_only! { - use outbound::services::site::SiteService; + use outbound::services::site::{SiteServiceProvider, SiteServiceImpl}; let store = tokio::runtime::Runtime::new().unwrap().block_on(setup_inmem_store()); builder = builder.with_context_provider(move || { - Box::new(SiteService::new(store.clone())) + Box::new(SiteServiceProvider::with(SiteServiceImpl::new(store.clone()))) }) } diff --git a/src/outbound/repository/adapters/memory.rs b/src/outbound/repository/adapters/memory.rs index 81974a0..8d89a57 100644 --- a/src/outbound/repository/adapters/memory.rs +++ b/src/outbound/repository/adapters/memory.rs @@ -175,6 +175,16 @@ impl SiteRepository for InMemoryStore { Ok(()) } + async fn delete_site(&self, domain: &str) -> Result<()> { + self.data + .lock() + .await + .sites + .remove(domain) + .ok_or(repository::site::Error::NotFound) + .map(|_| ()) + } + async fn get_page(&self, domain: &str, page: &str) -> Result<Page> { self.data .lock() @@ -246,7 +256,7 @@ impl SiteRepository for InMemoryStore { // Skip posts before and including the cursor post_id posts = posts .iter() - .skip_while(|(id, _)| id.to_owned() <= &after) + .skip_while(|(id, _)| **id <= after) .cloned() .collect(); } @@ -524,4 +534,27 @@ mod tests { repository::site::Error::Conflict )); } + + #[tokio::test] + async fn deletes_a_site() { + let store = InMemoryStore::with_test_data().await; + let _ = store.delete_site("example.com").await.unwrap(); + let result = store.get_site_by_domain("example.com").await; + assert!(result.is_err()); + assert!(matches!( + result.err().unwrap(), + repository::site::Error::NotFound + )); + } + + #[tokio::test] + async fn deletes_a_nonexistent_site() { + let store = InMemoryStore::new(); + let result = store.delete_site("example.com").await; + assert!(result.is_err()); + assert!(matches!( + result.err().unwrap(), + repository::site::Error::NotFound + )); + } } diff --git a/src/outbound/repository/site.rs b/src/outbound/repository/site.rs index 735ff5d..ebad384 100644 --- a/src/outbound/repository/site.rs +++ b/src/outbound/repository/site.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use thiserror::Error; use crate::domain::entities::{ @@ -12,13 +11,13 @@ pub struct SiteMetadata { pub title: String, } -#[async_trait] +#[async_trait::async_trait] pub trait SiteRepository: Send + Sync + 'static { async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata>; - async fn get_pages_for_site(&self, domain: &str) -> Result<Vec<PageInfo>>; - async fn create_site(&self, site: SiteMetadata) -> Result<()>; + async fn delete_site(&self, domain: &str) -> Result<()>; + async fn get_pages_for_site(&self, domain: &str) -> Result<Vec<PageInfo>>; async fn get_page(&self, domain: &str, name: &str) -> Result<Page>; async fn set_page(&self, domain: &str, page: Page) -> Result<()>; async fn delete_page(&self, domain: &str, page: &str) -> Result<()>; diff --git a/src/outbound/services/site.rs b/src/outbound/services/site.rs index 3300755..3214833 100644 --- a/src/outbound/services/site.rs +++ b/src/outbound/services/site.rs @@ -14,18 +14,51 @@ use crate::{ }; #[derive(Clone)] -pub struct SiteService { +pub struct SiteServiceProvider { + pub service: Arc<dyn SiteService>, +} + +impl SiteServiceProvider { + pub fn with(service: impl SiteService) -> Self { + Self { + service: Arc::new(service), + } + } +} + +#[async_trait::async_trait] +#[mockall::automock] +pub trait SiteService: Send + Sync + 'static { + async fn get_site(&self, domain: &str) -> Result<SiteInfo>; + async fn create_site(&self, site: SiteMetadata) -> Result<()>; + async fn delete_site(&self, domain: &str) -> Result<()>; + async fn get_page(&self, domain: &str, name: &str) -> Result<Page>; + async fn set_page(&self, domain: &str, page: Page) -> Result<()>; + async fn delete_page(&self, domain: &str, name: &str) -> Result<()>; + async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post>; + async fn get_posts( + &self, + domain: &str, + collection_id: &str, + cursor: Option<String>, + ) -> Result<Paginated<Post, String>>; +} + +pub struct SiteServiceImpl { site_repository: Arc<dyn SiteRepository>, } -impl SiteService { +impl SiteServiceImpl { pub fn new(site_repository: impl SiteRepository) -> Self { Self { site_repository: Arc::new(site_repository), } } +} - pub async fn get_site(&self, domain: &str) -> Result<SiteInfo> { +#[async_trait::async_trait] +impl SiteService for SiteServiceImpl { + async fn get_site(&self, domain: &str) -> Result<SiteInfo> { let info = self.site_repository.get_site_by_domain(domain).await?; let pages = self .site_repository @@ -39,31 +72,37 @@ impl SiteService { }) } - pub async fn create_site(&self, site: SiteMetadata) -> Result<()> { + async fn create_site(&self, site: SiteMetadata) -> Result<()> { self.site_repository.create_site(site).await?; Ok(()) } - pub async fn get_page(&self, domain: &str, name: &str) -> Result<Page> { + async fn delete_site(&self, domain: &str) -> Result<()> { + self.site_repository.delete_site(domain).await?; + + Ok(()) + } + + async fn get_page(&self, domain: &str, name: &str) -> Result<Page> { let page = self.site_repository.get_page(domain, name).await?; Ok(page) } - pub async fn set_page(&self, domain: &str, page: Page) -> Result<()> { + async fn set_page(&self, domain: &str, page: Page) -> Result<()> { self.site_repository.set_page(domain, page).await?; Ok(()) } - pub async fn delete_page(&self, domain: &str, page: &str) -> Result<()> { + async fn delete_page(&self, domain: &str, page: &str) -> Result<()> { self.site_repository.delete_page(domain, page).await?; Ok(()) } - pub async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post> { + async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post> { let post = self .site_repository .get_post(domain, collection_id, id) @@ -72,7 +111,7 @@ impl SiteService { Ok(post) } - pub async fn get_posts( + async fn get_posts( &self, domain: &str, collection_id: &str, @@ -119,12 +158,15 @@ mod tests { use super::{Error, SiteService}; use crate::{ domain::entities::site::{Block, Page, PageContent, PageInfo, Post}, - outbound::repository::{adapters::memory::InMemoryStore, site::SiteMetadata}, + outbound::{ + repository::{adapters::memory::InMemoryStore, site::SiteMetadata}, + services::site::SiteServiceImpl, + }, }; #[tokio::test] async fn gets_site_info() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let info = service.get_site("example.com").await.unwrap(); assert_eq!(info.domain, "example.com"); assert_eq!(info.title, "Test site"); @@ -133,7 +175,7 @@ mod tests { #[tokio::test] async fn gets_nonexistent_site() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let result = service.get_site("nonexistent.com").await; assert!(result.is_err()); assert!(matches!(result.err().unwrap(), Error::NotFound)); @@ -141,7 +183,7 @@ mod tests { #[tokio::test] async fn gets_page_data() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let page = service.get_page("example.com", "/").await.unwrap(); assert_eq!(page.info.title, "Home"); @@ -161,7 +203,7 @@ mod tests { #[tokio::test] async fn gets_nonexistent_page() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let result = service.get_page("example.com", "nonexistent").await; assert!(result.is_err()); assert!(matches!(result.err().unwrap(), Error::NotFound)); @@ -169,7 +211,7 @@ mod tests { #[tokio::test] async fn gets_a_single_post() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let post = service .get_post("example.com", "home_posts", "post_id") .await @@ -186,7 +228,7 @@ mod tests { #[tokio::test] async fn gets_nonexistent_post() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let result = service .get_post("example.com", "home_posts", "nonexistent") .await; @@ -196,7 +238,7 @@ mod tests { #[tokio::test] async fn gets_posts() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let posts = service .get_posts("example.com", "home_posts", None) .await @@ -207,7 +249,7 @@ mod tests { #[tokio::test] async fn gets_posts_with_cursor() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let posts = service .get_posts("example.com", "home_posts", Some("post_id".to_string())) .await @@ -218,7 +260,7 @@ mod tests { #[tokio::test] async fn gets_nonexistent_posts() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let result = service.get_posts("example.com", "nonexistent", None).await; assert!(result.is_err()); assert!(matches!(result.err().unwrap(), Error::NotFound)); @@ -234,7 +276,7 @@ mod tests { }, }; - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); service .set_page( "example.com", @@ -258,7 +300,7 @@ mod tests { #[tokio::test] async fn deletes_a_page() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); service.delete_page("example.com", "about").await.unwrap(); let result = service.get_page("example.com", "about").await; assert!(result.is_err()); @@ -267,7 +309,7 @@ mod tests { #[tokio::test] async fn deletes_a_nonexistent_page() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let result = service.delete_page("example.com", "nonexistent").await; assert!(result.is_err()); assert!(matches!(result.err().unwrap(), Error::NotFound)); @@ -275,7 +317,7 @@ mod tests { #[tokio::test] async fn creates_a_site() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); service .create_site(SiteMetadata { domain: "new.com".to_string(), @@ -287,7 +329,7 @@ mod tests { #[tokio::test] async fn doesnt_create_a_site_that_already_exists() { - let service = SiteService::new(InMemoryStore::with_test_data().await); + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); let result = service .create_site(SiteMetadata { domain: "example.com".to_string(), @@ -300,4 +342,21 @@ mod tests { Error::RepositoryError(crate::outbound::repository::site::Error::Conflict) )); } + + #[tokio::test] + async fn deletes_a_site() { + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); + service.delete_site("example.com").await.unwrap(); + let result = service.get_site("example.com").await; + assert!(result.is_err()); + assert!(matches!(result.err().unwrap(), Error::NotFound)); + } + + #[tokio::test] + async fn deletes_a_nonexistent_site() { + let service = SiteServiceImpl::new(InMemoryStore::with_test_data().await); + let result = service.delete_site("nonexistent").await; + assert!(result.is_err()); + assert!(matches!(result.err().unwrap(), Error::NotFound)); + } }