From 98b5746472c9c5d29d84fbb2baae958fae021f83 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 26 Oct 2023 12:32:38 +0200 Subject: [PATCH] add tests --- Cargo.lock | 4 +- crates/api_common/Cargo.toml | 2 + crates/api_common/src/context.rs | 51 +++++++++++- crates/api_common/src/utils.rs | 78 +++++++++++++++++-- crates/api_crud/src/community/create.rs | 28 +++++-- crates/apub/Cargo.toml | 2 - crates/apub/src/api/user_settings_backup.rs | 9 +-- .../src/collections/community_moderators.rs | 8 +- crates/apub/src/objects/comment.rs | 5 +- crates/apub/src/objects/community.rs | 4 +- crates/apub/src/objects/instance.rs | 4 +- crates/apub/src/objects/mod.rs | 55 ------------- crates/apub/src/objects/person.rs | 9 +-- crates/apub/src/objects/post.rs | 5 +- crates/apub/src/objects/private_message.rs | 5 +- crates/db_schema/src/utils.rs | 46 +---------- 16 files changed, 163 insertions(+), 152 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 319caa769..e32eab11b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2578,6 +2578,7 @@ dependencies = [ "activitypub_federation", "actix-web", "anyhow", + "async-trait", "chrono", "encoding", "enum-map", @@ -2598,6 +2599,7 @@ dependencies = [ "serde", "serde_with", "serial_test", + "task-local-extensions", "tokio", "tracing", "ts-rs", @@ -2653,14 +2655,12 @@ dependencies = [ "moka", "once_cell", "reqwest", - "reqwest-middleware", "serde", "serde_json", "serde_with", "serial_test", "stringreader", "strum_macros", - "task-local-extensions", "tokio", "tracing", "url", diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index c3a7fa8d9..53a1093aa 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -63,6 +63,7 @@ once_cell = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } enum-map = { workspace = true } urlencoding = { workspace = true } +async-trait = { workspace = true } webpage = { version = "1.6", default-features = false, features = [ "serde", ], optional = true } @@ -70,6 +71,7 @@ encoding = { version = "0.2.33", optional = true } jsonwebtoken = { version = "8.3.0", optional = true } # necessary for wasmt compilation getrandom = { version = "0.2.10", features = ["js"] } +task-local-extensions = "0.1.4" [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/api_common/src/context.rs b/crates/api_common/src/context.rs index 888a98741..b8eeda3e9 100644 --- a/crates/api_common/src/context.rs +++ b/crates/api_common/src/context.rs @@ -1,13 +1,18 @@ +use crate::request::client_builder; +use activitypub_federation::config::{Data, FederationConfig}; +use anyhow::anyhow; use lemmy_db_schema::{ source::secret::Secret, - utils::{ActualDbPool, DbPool}, + utils::{build_db_pool_for_tests, ActualDbPool, DbPool}, }; use lemmy_utils::{ rate_limit::RateLimitCell, settings::{structs::Settings, SETTINGS}, }; -use reqwest_middleware::ClientWithMiddleware; +use reqwest::{Request, Response}; +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, Middleware, Next}; use std::sync::Arc; +use task_local_extensions::Extensions; #[derive(Clone)] pub struct LemmyContext { @@ -49,4 +54,46 @@ impl LemmyContext { pub fn rate_limit_cell(&self) -> &RateLimitCell { &self.rate_limit_cell } + + /// Initialize a context for use in tests, doesn't allow network requests. + /// + /// Do not use this in production code. + pub async fn init_test_context() -> Data { + // call this to run migrations + let pool = build_db_pool_for_tests().await; + + let client = client_builder(&SETTINGS).build().expect("build client"); + + let client = ClientBuilder::new(client).with(BlockedMiddleware).build(); + let secret = Secret { + id: 0, + jwt_secret: String::new(), + }; + + let rate_limit_cell = RateLimitCell::with_test_config(); + + let context = LemmyContext::create(pool, client, secret, rate_limit_cell.clone()); + let config = FederationConfig::builder() + .domain(context.settings().hostname.clone()) + .app_data(context) + .build() + .await + .expect("build federation config"); + config.to_request_data() + } +} + +struct BlockedMiddleware; + +/// A reqwest middleware which blocks all requests +#[async_trait::async_trait] +impl Middleware for BlockedMiddleware { + async fn handle( + &self, + _req: Request, + _extensions: &mut Extensions, + _next: Next<'_>, + ) -> reqwest_middleware::Result { + Err(anyhow!("Network requests not allowed").into()) + } } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index dde9b9612..108b2b7d3 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -24,7 +24,7 @@ use lemmy_db_schema::{ post::{Post, PostRead}, }, traits::Crud, - utils::{diesel_option_overwrite_to_url, DbPool}, + utils::DbPool, }; use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; use lemmy_db_views_actor::structs::{ @@ -37,7 +37,7 @@ use lemmy_utils::{ error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, location_info, rate_limit::{ActionType, BucketConfig}, - settings::{structs::Settings, SETTINGS}, + settings::structs::Settings, utils::{ markdown::markdown_rewrite_image_links, slurs::{build_slur_regex, remove_slurs}, @@ -815,13 +815,13 @@ pub async fn process_markdown_opt( pub async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult { // Dont rewrite links pointing to local domain. - if link.domain() == Some(&SETTINGS.hostname) { + if link.domain() == Some(&context.settings().hostname) { return Ok(link.into()); } let proxied = format!( "{}/api/v3/image_proxy?url={}", - SETTINGS.get_protocol_and_hostname(), + context.settings().get_protocol_and_hostname(), encode(link.as_str()) ); RemoteImage::create(&mut context.pool(), vec![link]).await?; @@ -832,21 +832,30 @@ pub async fn proxy_image_link_opt_api( link: &Option, context: &LemmyContext, ) -> LemmyResult>> { - let link = diesel_option_overwrite_to_url(link)?; - if let Some(l) = link { - proxy_image_link_opt_apub(l.map(Into::into), context) + let link: Option> = match link.as_ref().map(String::as_str) { + // An empty string is an erase + Some("") => Some(None), + Some(str_url) => Url::parse(str_url) + .map(|u| Some(Some(u.into()))) + .with_lemmy_type(LemmyErrorType::InvalidUrl)?, + None => None, + }; + if let Some(Some(l)) = link { + proxy_image_link(l.into(), context) .await .map(Some) + .map(Some) } else { Ok(link) } } + pub async fn proxy_image_link_opt_apub( link: Option, context: &LemmyContext, ) -> LemmyResult> { if let Some(l) = link { - proxy_image_link(l.clone(), context).await.map(Some) + proxy_image_link(l, context).await.map(Some) } else { Ok(None) } @@ -857,8 +866,10 @@ mod tests { #![allow(clippy::unwrap_used)] #![allow(clippy::indexing_slicing)] + use super::*; use crate::utils::{honeypot_check, limit_expire_time, password_length_check}; use chrono::{Days, Utc}; + use serial_test::serial; #[test] #[rustfmt::skip] @@ -897,4 +908,55 @@ mod tests { None ); } + + #[tokio::test] + #[serial] + async fn test_proxy_image_link() { + let context = LemmyContext::init_test_context().await; + + // image from local domain is unchanged + let local_url = Url::parse("http://lemmy-alpha/image.png").unwrap(); + let proxied = proxy_image_link(local_url.clone(), &context).await.unwrap(); + assert_eq!(&local_url, proxied.inner()); + + // image from remote domain is proxied + let remote_image = Url::parse("http://lemmy-beta/image.png").unwrap(); + let proxied = proxy_image_link(remote_image.clone(), &context) + .await + .unwrap(); + assert_eq!( + "https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", + proxied.as_str() + ); + assert!( + RemoteImage::validate(&mut context.pool(), remote_image.into()) + .await + .is_ok() + ); + } + + #[tokio::test] + #[serial] + async fn test_diesel_option_overwrite_to_url() { + let context = LemmyContext::init_test_context().await; + + assert!(matches!( + proxy_image_link_opt_api(&None, &context).await, + Ok(None) + )); + assert!(matches!( + proxy_image_link_opt_api(&Some(String::new()), &context).await, + Ok(Some(None)) + )); + assert!( + proxy_image_link_opt_api(&Some("invalid_url".to_string()), &context) + .await + .is_err() + ); + let example_url = "https://lemmy-alpha/image.png"; + assert!(matches!( + proxy_image_link_opt_api(&Some(example_url.to_string()), &context).await, + Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into() + )); + } } diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index edd51ce25..656188972 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -12,11 +12,12 @@ use lemmy_api_common::{ is_admin, local_site_to_slur_regex, process_markdown_opt, - proxy_image_link_opt_api, + proxy_image_link, EndpointType, }, }; use lemmy_db_schema::{ + newtypes::DbUrl, source::{ actor_language::{CommunityLanguage, SiteLanguage}, community::{ @@ -38,6 +39,7 @@ use lemmy_utils::{ validation::{is_valid_actor_name, is_valid_body_field}, }, }; +use url::Url; #[tracing::instrument(skip(context))] pub async fn create_community( @@ -56,12 +58,8 @@ pub async fn create_community( check_slurs(&data.name, &slur_regex)?; check_slurs(&data.title, &slur_regex)?; let description = process_markdown_opt(&data.description, &slur_regex, &context).await?; - let icon = proxy_image_link_opt_api(&data.icon, &context) - .await? - .unwrap(); - let banner = proxy_image_link_opt_api(&data.banner, &context) - .await? - .unwrap(); + let icon = proxy_image_link_create(&data.icon, &context).await?; + let banner = proxy_image_link_create(&data.banner, &context).await?; is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?; is_valid_body_field(&data.description, false)?; @@ -138,3 +136,19 @@ pub async fn create_community( build_community_response(&context, local_user_view, community_id).await } + +async fn proxy_image_link_create( + opt: &Option, + context: &LemmyContext, +) -> Result, LemmyError> { + match opt.as_ref().map(String::as_str) { + // An empty string is nothing + Some("") => Ok(None), + Some(str_url) => { + let url = Url::parse(str_url).with_lemmy_type(LemmyErrorType::InvalidUrl)?; + let url = proxy_image_link(url, context).await?; + Ok(Some(url)) + } + None => Ok(None), + } +} diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 748fe3335..66f46392b 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -46,6 +46,4 @@ moka = { version = "0.11", features = ["future"] } [dev-dependencies] serial_test = { workspace = true } -reqwest-middleware = { workspace = true } -task-local-extensions = "0.1.4" assert-json-diff = "2.0.2" diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 419be280d..8959b0ad5 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -280,10 +280,7 @@ mod tests { #![allow(clippy::unwrap_used)] #![allow(clippy::indexing_slicing)] - use crate::{ - api::user_settings_backup::{export_settings, import_settings}, - objects::tests::init_context, - }; + use crate::api::user_settings_backup::{export_settings, import_settings}; use activitypub_federation::config::Data; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ @@ -337,7 +334,7 @@ mod tests { #[tokio::test] #[serial] async fn test_settings_export_import() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let export_user = create_user("hanna".to_string(), Some("my bio".to_string()), &context).await; @@ -398,7 +395,7 @@ mod tests { #[tokio::test] #[serial] async fn disallow_large_backup() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let export_user = create_user("hanna".to_string(), Some("my bio".to_string()), &context).await; diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index cdaf985ea..9d31e448e 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -107,11 +107,7 @@ mod tests { use super::*; use crate::{ - objects::{ - community::tests::parse_lemmy_community, - person::tests::parse_lemmy_person, - tests::init_context, - }, + objects::{community::tests::parse_lemmy_community, person::tests::parse_lemmy_person}, protocol::tests::file_to_json_object, }; use lemmy_db_schema::{ @@ -128,7 +124,7 @@ mod tests { #[tokio::test] #[serial] async fn test_parse_lemmy_community_moderators() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let (new_mod, site) = parse_lemmy_person(&context).await; let community = parse_lemmy_community(&context).await; let community_id = community.id; diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 8b524433a..036c9be28 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -196,7 +196,6 @@ pub(crate) mod tests { instance::ApubSite, person::{tests::parse_lemmy_person, ApubPerson}, post::ApubPost, - tests::init_context, }, protocol::tests::file_to_json_object, }; @@ -234,7 +233,7 @@ pub(crate) mod tests { #[tokio::test] #[serial] pub(crate) async fn test_parse_lemmy_comment() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let url = Url::parse("https://enterprise.lemmy.ml/comment/38741").unwrap(); let data = prepare_comment_test(&url, &context).await; @@ -262,7 +261,7 @@ pub(crate) mod tests { #[tokio::test] #[serial] async fn test_parse_pleroma_comment() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let url = Url::parse("https://enterprise.lemmy.ml/comment/38741").unwrap(); let data = prepare_comment_test(&url, &context).await; diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index da8428db0..0a9a76fc8 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -261,7 +261,7 @@ pub(crate) mod tests { use super::*; use crate::{ - objects::{instance::tests::parse_lemmy_instance, tests::init_context}, + objects::instance::tests::parse_lemmy_instance, protocol::tests::file_to_json_object, }; use activitypub_federation::fetch::collection_id::CollectionId; @@ -290,7 +290,7 @@ pub(crate) mod tests { #[tokio::test] #[serial] async fn test_parse_lemmy_community() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let site = parse_lemmy_instance(&context).await; let community = parse_lemmy_community(&context).await; diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index b7358e70f..0a17b509c 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -218,7 +218,7 @@ pub(crate) mod tests { #![allow(clippy::indexing_slicing)] use super::*; - use crate::{objects::tests::init_context, protocol::tests::file_to_json_object}; + use crate::protocol::tests::file_to_json_object; use lemmy_db_schema::traits::Crud; use serial_test::serial; @@ -234,7 +234,7 @@ pub(crate) mod tests { #[tokio::test] #[serial] async fn test_parse_lemmy_instance() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let site = parse_lemmy_instance(&context).await; assert_eq!(site.name, "Enterprise"); diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 116c7f4fb..cabd07e6d 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -51,58 +51,3 @@ pub(crate) fn verify_is_remote_object(id: &Url, settings: &Settings) -> Result<( Ok(()) } } - -#[cfg(test)] -pub(crate) mod tests { - #![allow(clippy::unwrap_used)] - #![allow(clippy::indexing_slicing)] - - use activitypub_federation::config::{Data, FederationConfig}; - use anyhow::anyhow; - use lemmy_api_common::{context::LemmyContext, request::client_builder}; - use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool_for_tests}; - use lemmy_utils::{rate_limit::RateLimitCell, settings::SETTINGS}; - use reqwest::{Request, Response}; - use reqwest_middleware::{ClientBuilder, Middleware, Next}; - use task_local_extensions::Extensions; - - struct BlockedMiddleware; - - /// A reqwest middleware which blocks all requests - #[async_trait::async_trait] - impl Middleware for BlockedMiddleware { - async fn handle( - &self, - _req: Request, - _extensions: &mut Extensions, - _next: Next<'_>, - ) -> reqwest_middleware::Result { - Err(anyhow!("Network requests not allowed").into()) - } - } - - // TODO: would be nice if we didnt have to use a full context for tests. - pub(crate) async fn init_context() -> Data { - // call this to run migrations - let pool = build_db_pool_for_tests().await; - - let client = client_builder(&SETTINGS).build().unwrap(); - - let client = ClientBuilder::new(client).with(BlockedMiddleware).build(); - let secret = Secret { - id: 0, - jwt_secret: String::new(), - }; - - let rate_limit_cell = RateLimitCell::with_test_config(); - - let context = LemmyContext::create(pool, client, secret, rate_limit_cell.clone()); - let config = FederationConfig::builder() - .domain("example.com") - .app_data(context) - .build() - .await - .unwrap(); - config.to_request_data() - } -} diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 4dec4d8b0..d4c9eabbd 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -224,10 +224,7 @@ pub(crate) mod tests { use super::*; use crate::{ - objects::{ - instance::{tests::parse_lemmy_instance, ApubSite}, - tests::init_context, - }, + objects::instance::{tests::parse_lemmy_instance, ApubSite}, protocol::{objects::instance::Instance, tests::file_to_json_object}, }; use activitypub_federation::fetch::object_id::ObjectId; @@ -247,7 +244,7 @@ pub(crate) mod tests { #[tokio::test] #[serial] async fn test_parse_lemmy_person() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let (person, site) = parse_lemmy_person(&context).await; assert_eq!(person.display_name, Some("Jean-Luc Picard".to_string())); @@ -260,7 +257,7 @@ pub(crate) mod tests { #[tokio::test] #[serial] async fn test_parse_pleroma_person() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; // create and parse a fake pleroma instance actor, to avoid network request during test let mut json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap(); diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index f7c799e97..b8953c984 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -303,7 +303,6 @@ mod tests { instance::ApubSite, person::{tests::parse_lemmy_person, ApubPerson}, post::ApubPost, - tests::init_context, }, protocol::tests::file_to_json_object, }; @@ -313,7 +312,7 @@ mod tests { #[tokio::test] #[serial] async fn test_parse_lemmy_post() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let (person, site) = parse_lemmy_person(&context).await; let community = parse_lemmy_community(&context).await; @@ -336,7 +335,7 @@ mod tests { #[tokio::test] #[serial] async fn test_convert_mastodon_post_title() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let (person, site) = parse_lemmy_person(&context).await; let community = parse_lemmy_community(&context).await; diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 87d956a35..4bac77a09 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -156,7 +156,6 @@ mod tests { objects::{ instance::{tests::parse_lemmy_instance, ApubSite}, person::ApubPerson, - tests::init_context, }, protocol::tests::file_to_json_object, }; @@ -201,7 +200,7 @@ mod tests { #[tokio::test] #[serial] async fn test_parse_lemmy_pm() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap(); let data = prepare_comment_test(&url, &context).await; let json: ChatMessage = file_to_json_object("assets/lemmy/objects/chat_message.json").unwrap(); @@ -229,7 +228,7 @@ mod tests { #[tokio::test] #[serial] async fn test_parse_pleroma_pm() { - let context = init_context().await; + let context = LemmyContext::init_test_context().await; let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap(); let data = prepare_comment_test(&url, &context).await; let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2").unwrap(); diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 7e83569a7..4e25c791b 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -28,10 +28,7 @@ use diesel_async::{ }; use diesel_migrations::EmbeddedMigrations; use futures_util::{future::BoxFuture, Future, FutureExt}; -use lemmy_utils::{ - error::{LemmyError, LemmyErrorExt, LemmyErrorType}, - settings::structs::Settings, -}; +use lemmy_utils::{error::LemmyError, settings::structs::Settings}; use once_cell::sync::Lazy; use regex::Regex; use rustls::{ @@ -212,32 +209,6 @@ pub fn diesel_option_overwrite(opt: Option) -> Option> { } } -pub fn diesel_option_overwrite_to_url( - opt: &Option, -) -> Result>, LemmyError> { - match opt.as_ref().map(String::as_str) { - // An empty string is an erase - Some("") => Ok(Some(None)), - Some(str_url) => Url::parse(str_url) - .map(|u| Some(Some(u.into()))) - .with_lemmy_type(LemmyErrorType::InvalidUrl), - None => Ok(None), - } -} - -pub fn diesel_option_overwrite_to_url_create( - opt: &Option, -) -> Result, LemmyError> { - match opt.as_ref().map(String::as_str) { - // An empty string is nothing - Some("") => Ok(None), - Some(str_url) => Url::parse(str_url) - .map(|u| Some(u.into())) - .with_lemmy_type(LemmyErrorType::InvalidUrl), - None => Ok(None), - } -} - async fn build_db_pool_settings_opt( settings: Option<&Settings>, ) -> Result { @@ -516,19 +487,4 @@ mod tests { Some(Some("test".to_string())) ); } - - #[test] - fn test_diesel_option_overwrite_to_url() { - assert!(matches!(diesel_option_overwrite_to_url(&None), Ok(None))); - assert!(matches!( - diesel_option_overwrite_to_url(&Some(String::new())), - Ok(Some(None)) - )); - assert!(diesel_option_overwrite_to_url(&Some("invalid_url".to_string())).is_err()); - let example_url = "https://example.com"; - assert!(matches!( - diesel_option_overwrite_to_url(&Some(example_url.to_string())), - Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into() - )); - } }