diff --git a/crates/adapters/jellyfin/src/lib.rs b/crates/adapters/jellyfin/src/lib.rs index 1ee790bf..3ff9cc91 100644 --- a/crates/adapters/jellyfin/src/lib.rs +++ b/crates/adapters/jellyfin/src/lib.rs @@ -60,73 +60,5 @@ struct JellyfinPayload { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_valid_playback_stop() { - let body = serde_json::json!({ - "NotificationType": "PlaybackStop", - "ItemType": "Movie", - "Name": "Blade Runner", - "Year": 1982, - "PlayedToCompletion": true, - "Provider_tmdb": "78", - "Provider_imdb": "tt0083658" - }); - let parser = JellyfinParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - let event = result.expect("should parse"); - assert_eq!(event.title, "Blade Runner"); - assert_eq!(event.year, Some(1982)); - assert_eq!(event.tmdb_id, Some("tmdb:78".into())); - assert_eq!(event.imdb_id, Some("tt0083658".into())); - } - - #[test] - fn ignores_non_movie() { - let body = serde_json::json!({ - "NotificationType": "PlaybackStop", - "ItemType": "Episode", - "Name": "Some Episode", - "PlayedToCompletion": true - }); - let parser = JellyfinParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - assert!(result.is_none()); - } - - #[test] - fn ignores_incomplete_playback() { - let body = serde_json::json!({ - "NotificationType": "PlaybackStop", - "ItemType": "Movie", - "Name": "Blade Runner", - "PlayedToCompletion": false - }); - let parser = JellyfinParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - assert!(result.is_none()); - } - - #[test] - fn ignores_playback_start() { - let body = serde_json::json!({ - "NotificationType": "PlaybackStart", - "ItemType": "Movie", - "Name": "Blade Runner", - "PlayedToCompletion": false - }); - let parser = JellyfinParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - assert!(result.is_none()); - } -} +#[path = "tests/lib.rs"] +mod tests; diff --git a/crates/adapters/jellyfin/src/tests/lib.rs b/crates/adapters/jellyfin/src/tests/lib.rs new file mode 100644 index 00000000..9511eb78 --- /dev/null +++ b/crates/adapters/jellyfin/src/tests/lib.rs @@ -0,0 +1,68 @@ +use super::*; + +#[test] +fn parses_valid_playback_stop() { + let body = serde_json::json!({ + "NotificationType": "PlaybackStop", + "ItemType": "Movie", + "Name": "Blade Runner", + "Year": 1982, + "PlayedToCompletion": true, + "Provider_tmdb": "78", + "Provider_imdb": "tt0083658" + }); + let parser = JellyfinParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + let event = result.expect("should parse"); + assert_eq!(event.title, "Blade Runner"); + assert_eq!(event.year, Some(1982)); + assert_eq!(event.tmdb_id, Some("tmdb:78".into())); + assert_eq!(event.imdb_id, Some("tt0083658".into())); +} + +#[test] +fn ignores_non_movie() { + let body = serde_json::json!({ + "NotificationType": "PlaybackStop", + "ItemType": "Episode", + "Name": "Some Episode", + "PlayedToCompletion": true + }); + let parser = JellyfinParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + assert!(result.is_none()); +} + +#[test] +fn ignores_incomplete_playback() { + let body = serde_json::json!({ + "NotificationType": "PlaybackStop", + "ItemType": "Movie", + "Name": "Blade Runner", + "PlayedToCompletion": false + }); + let parser = JellyfinParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + assert!(result.is_none()); +} + +#[test] +fn ignores_playback_start() { + let body = serde_json::json!({ + "NotificationType": "PlaybackStart", + "ItemType": "Movie", + "Name": "Blade Runner", + "PlayedToCompletion": false + }); + let parser = JellyfinParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + assert!(result.is_none()); +} diff --git a/crates/adapters/plex/src/lib.rs b/crates/adapters/plex/src/lib.rs index 685b4108..2c6dfc05 100644 --- a/crates/adapters/plex/src/lib.rs +++ b/crates/adapters/plex/src/lib.rs @@ -70,100 +70,5 @@ struct PlexGuid { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_movie_scrobble() { - let body = serde_json::json!({ - "event": "media.scrobble", - "Metadata": { - "type": "movie", - "title": "Blade Runner", - "year": 1982, - "Guid": [ - {"id": "tmdb://78"}, - {"id": "imdb://tt0083658"} - ] - } - }); - let parser = PlexParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - let event = result.expect("should parse"); - assert_eq!(event.title, "Blade Runner"); - assert_eq!(event.year, Some(1982)); - assert_eq!(event.tmdb_id, Some("tmdb:78".into())); - assert_eq!(event.imdb_id, Some("tt0083658".into())); - } - - #[test] - fn ignores_tv_episode() { - let body = serde_json::json!({ - "event": "media.scrobble", - "Metadata": { - "type": "episode", - "title": "Pilot", - "grandparentTitle": "Breaking Bad", - "year": 2008, - "Guid": [] - } - }); - let parser = PlexParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - assert!(result.is_none()); - } - - #[test] - fn ignores_play_event() { - let body = serde_json::json!({ - "event": "media.play", - "Metadata": { - "type": "movie", - "title": "Blade Runner", - "year": 1982, - "Guid": [] - } - }); - let parser = PlexParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - assert!(result.is_none()); - } - - #[test] - fn handles_no_guids() { - let body = serde_json::json!({ - "event": "media.scrobble", - "Metadata": { - "type": "movie", - "title": "Some Indie Film", - "year": 2023 - } - }); - let parser = PlexParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - let event = result.expect("should parse"); - assert_eq!(event.title, "Some Indie Film"); - assert!(event.tmdb_id.is_none()); - assert!(event.imdb_id.is_none()); - } - - #[test] - fn handles_missing_metadata() { - let body = serde_json::json!({ - "event": "media.scrobble" - }); - let parser = PlexParser; - let result = parser - .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) - .unwrap(); - assert!(result.is_none()); - } -} +#[path = "tests/lib.rs"] +mod tests; diff --git a/crates/adapters/plex/src/tests/lib.rs b/crates/adapters/plex/src/tests/lib.rs new file mode 100644 index 00000000..2459f256 --- /dev/null +++ b/crates/adapters/plex/src/tests/lib.rs @@ -0,0 +1,95 @@ +use super::*; + +#[test] +fn parses_movie_scrobble() { + let body = serde_json::json!({ + "event": "media.scrobble", + "Metadata": { + "type": "movie", + "title": "Blade Runner", + "year": 1982, + "Guid": [ + {"id": "tmdb://78"}, + {"id": "imdb://tt0083658"} + ] + } + }); + let parser = PlexParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + let event = result.expect("should parse"); + assert_eq!(event.title, "Blade Runner"); + assert_eq!(event.year, Some(1982)); + assert_eq!(event.tmdb_id, Some("tmdb:78".into())); + assert_eq!(event.imdb_id, Some("tt0083658".into())); +} + +#[test] +fn ignores_tv_episode() { + let body = serde_json::json!({ + "event": "media.scrobble", + "Metadata": { + "type": "episode", + "title": "Pilot", + "grandparentTitle": "Breaking Bad", + "year": 2008, + "Guid": [] + } + }); + let parser = PlexParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + assert!(result.is_none()); +} + +#[test] +fn ignores_play_event() { + let body = serde_json::json!({ + "event": "media.play", + "Metadata": { + "type": "movie", + "title": "Blade Runner", + "year": 1982, + "Guid": [] + } + }); + let parser = PlexParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + assert!(result.is_none()); +} + +#[test] +fn handles_no_guids() { + let body = serde_json::json!({ + "event": "media.scrobble", + "Metadata": { + "type": "movie", + "title": "Some Indie Film", + "year": 2023 + } + }); + let parser = PlexParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + let event = result.expect("should parse"); + assert_eq!(event.title, "Some Indie Film"); + assert!(event.tmdb_id.is_none()); + assert!(event.imdb_id.is_none()); +} + +#[test] +fn handles_missing_metadata() { + let body = serde_json::json!({ + "event": "media.scrobble" + }); + let parser = PlexParser; + let result = parser + .parse_playback_event(serde_json::to_vec(&body).unwrap().as_slice()) + .unwrap(); + assert!(result.is_none()); +} diff --git a/crates/adapters/rss/src/lib.rs b/crates/adapters/rss/src/lib.rs index d11d5ff8..87057c4e 100644 --- a/crates/adapters/rss/src/lib.rs +++ b/crates/adapters/rss/src/lib.rs @@ -1,3 +1,4 @@ +use domain::errors::DomainError; use domain::models::DiaryEntry; use domain::ports::{FeedChannel, RssFeedRenderer}; use rss_feed::extension::atom::{self, AtomExtensionBuilder, Link}; @@ -24,7 +25,7 @@ impl RssFeedRenderer for RssAdapter { &self, entries: &[DiaryEntry], channel: &FeedChannel<'_>, - ) -> Result { + ) -> Result { let items = entries .iter() .map(|e| { diff --git a/crates/api-types/src/social.rs b/crates/api-types/src/social.rs index 94af24e0..f7172b78 100644 --- a/crates/api-types/src/social.rs +++ b/crates/api-types/src/social.rs @@ -78,28 +78,6 @@ pub struct PendingCountResponse { pub count: usize, } -//todo: move out to separate file #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn follow_state_dto_serializes_to_lowercase_strings() { - assert_eq!( - serde_json::to_string(&FollowStateDto::None).unwrap(), - "\"none\"" - ); - assert_eq!( - serde_json::to_string(&FollowStateDto::Pending).unwrap(), - "\"pending\"" - ); - assert_eq!( - serde_json::to_string(&FollowStateDto::Accepted).unwrap(), - "\"accepted\"" - ); - assert_eq!( - serde_json::to_string(&FollowStateDto::Rejected).unwrap(), - "\"rejected\"" - ); - } -} +#[path = "tests/social.rs"] +mod tests; diff --git a/crates/api-types/src/tests/social.rs b/crates/api-types/src/tests/social.rs new file mode 100644 index 00000000..2e09621b --- /dev/null +++ b/crates/api-types/src/tests/social.rs @@ -0,0 +1,21 @@ +use super::*; + +#[test] +fn follow_state_dto_serializes_to_lowercase_strings() { + assert_eq!( + serde_json::to_string(&FollowStateDto::None).unwrap(), + "\"none\"" + ); + assert_eq!( + serde_json::to_string(&FollowStateDto::Pending).unwrap(), + "\"pending\"" + ); + assert_eq!( + serde_json::to_string(&FollowStateDto::Accepted).unwrap(), + "\"accepted\"" + ); + assert_eq!( + serde_json::to_string(&FollowStateDto::Rejected).unwrap(), + "\"rejected\"" + ); +} diff --git a/crates/application/src/import/execute.rs b/crates/application/src/import/execute.rs index 6480cde3..73df7b7a 100644 --- a/crates/application/src/import/execute.rs +++ b/crates/application/src/import/execute.rs @@ -44,7 +44,8 @@ pub async fn execute( let mut failed = Vec::new(); let semaphore = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY_LIMIT)); - let mut tasks: tokio::task::JoinSet<(usize, Result<(), String>)> = tokio::task::JoinSet::new(); + let mut tasks: tokio::task::JoinSet<(usize, Result<(), DomainError>)> = + tokio::task::JoinSet::new(); for (idx, annotated) in row_results.into_iter().enumerate() { if !confirmed_set.contains(&idx) { @@ -56,12 +57,12 @@ pub async fn execute( failed.push((idx, errors.join("; "))); } RowResult::Valid(row) => match row_to_command(&row, user_id.value()) { - Err(e) => failed.push((idx, e)), + Err(e) => failed.push((idx, import_error_message(e))), Ok(log_cmd) => { let permit = Arc::clone(&semaphore).acquire_owned().await.unwrap(); let logger = deps.review_logger.clone(); tasks.spawn(async move { - let result = logger.log_review(log_cmd).await.map_err(|e| e.to_string()); + let result = logger.log_review(log_cmd).await; drop(permit); (idx, result) }); @@ -74,7 +75,7 @@ pub async fn execute( let (idx, outcome) = res.expect("import task panicked"); match outcome { Ok(()) => imported += 1, - Err(e) => failed.push((idx, e)), + Err(e) => failed.push((idx, e.to_string())), } } @@ -91,20 +92,36 @@ pub async fn execute( #[path = "tests/execute.rs"] mod tests; -fn row_to_command(row: &ImportRow, user_id: Uuid) -> Result { +/// Row-level validation failures keep their bare message in the summary. +fn import_error_message(e: DomainError) -> String { + match e { + DomainError::ValidationError(msg) => msg, + other => other.to_string(), + } +} + +fn row_to_command(row: &ImportRow, user_id: Uuid) -> Result { let rating = row .rating .as_deref() - .ok_or("missing rating")? + .ok_or_else(|| DomainError::ValidationError("missing rating".into()))? .parse::() - .map_err(|_| "rating is not a valid u8".to_string())?; + .map_err(|_| DomainError::ValidationError("rating is not a valid u8".into()))?; - let watched_at_str = row.watched_at.as_deref().ok_or("missing watched_at")?; + let watched_at_str = row + .watched_at + .as_deref() + .ok_or_else(|| DomainError::ValidationError("missing watched_at".into()))?; let watched_at = NaiveDateTime::parse_from_str(&format!("{} 00:00:00", watched_at_str), "%Y-%m-%d %H:%M:%S") .or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%d %H:%M:%S")) .or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%dT%H:%M:%S")) - .map_err(|_| format!("cannot parse watched_at: '{}'", watched_at_str))?; + .map_err(|_| { + DomainError::ValidationError(format!( + "cannot parse watched_at: '{}'", + watched_at_str + )) + })?; Ok(LogReviewCommand { user_id, diff --git a/crates/application/src/users/diary_filter.rs b/crates/application/src/users/diary_filter.rs index 326b9cf2..e52744e8 100644 --- a/crates/application/src/users/diary_filter.rs +++ b/crates/application/src/users/diary_filter.rs @@ -38,43 +38,5 @@ pub(super) fn paged_user_filter( } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn feed_sort_to_direction_all_variants() { - assert!(matches!( - feed_sort_to_direction(FeedSortBy::Date), - ReviewSortBy::Descending - )); - assert!(matches!( - feed_sort_to_direction(FeedSortBy::DateAsc), - ReviewSortBy::Ascending - )); - assert!(matches!( - feed_sort_to_direction(FeedSortBy::Rating), - ReviewSortBy::ByRatingDesc - )); - assert!(matches!( - feed_sort_to_direction(FeedSortBy::RatingAsc), - ReviewSortBy::ByRatingAsc - )); - } - - #[test] - fn paged_user_filter_builds_correctly() { - let uid = UserId::from_uuid(uuid::Uuid::new_v4()); - let filter = paged_user_filter( - uid.clone(), - ReviewSortBy::Descending, - Some(20), - Some(5), - Some("blade".into()), - false, - ) - .unwrap(); - - assert_eq!(filter.user_id.unwrap().value(), uid.value()); - assert_eq!(filter.search.as_deref(), Some("blade")); - } -} +#[path = "tests/diary_filter.rs"] +mod tests; diff --git a/crates/application/src/users/tests/diary_filter.rs b/crates/application/src/users/tests/diary_filter.rs new file mode 100644 index 00000000..1bdbe433 --- /dev/null +++ b/crates/application/src/users/tests/diary_filter.rs @@ -0,0 +1,38 @@ +use super::*; + +#[test] +fn feed_sort_to_direction_all_variants() { + assert!(matches!( + feed_sort_to_direction(FeedSortBy::Date), + ReviewSortBy::Descending + )); + assert!(matches!( + feed_sort_to_direction(FeedSortBy::DateAsc), + ReviewSortBy::Ascending + )); + assert!(matches!( + feed_sort_to_direction(FeedSortBy::Rating), + ReviewSortBy::ByRatingDesc + )); + assert!(matches!( + feed_sort_to_direction(FeedSortBy::RatingAsc), + ReviewSortBy::ByRatingAsc + )); +} + +#[test] +fn paged_user_filter_builds_correctly() { + let uid = UserId::from_uuid(uuid::Uuid::new_v4()); + let filter = paged_user_filter( + uid.clone(), + ReviewSortBy::Descending, + Some(20), + Some(5), + Some("blade".into()), + false, + ) + .unwrap(); + + assert_eq!(filter.user_id.unwrap().value(), uid.value()); + assert_eq!(filter.search.as_deref(), Some("blade")); +} diff --git a/crates/domain/src/ports/federation.rs b/crates/domain/src/ports/federation.rs index 6ec92eb4..9c944d8a 100644 --- a/crates/domain/src/ports/federation.rs +++ b/crates/domain/src/ports/federation.rs @@ -51,47 +51,5 @@ pub trait ApBackfillPort: Send + Sync { } #[cfg(test)] -mod tests { - use super::*; - use crate::ports::noop::{NoopApBackfill, NoopApDocument, NoopInstanceBlocklist}; - - #[tokio::test] - async fn noop_document_returns_empty_strings() { - let p = NoopApDocument; - assert_eq!(p.actor_json("anything").await.unwrap(), ""); - assert_eq!( - p.followers_collection_json(uuid::Uuid::nil(), None) - .await - .unwrap(), - "" - ); - assert_eq!( - p.following_collection_json(uuid::Uuid::nil(), Some(2)) - .await - .unwrap(), - "" - ); - } - - #[tokio::test] - async fn noop_blocklist_returns_empty_and_ok() { - let p = NoopInstanceBlocklist; - assert!(p.get_blocked_domains().await.unwrap().is_empty()); - p.add_blocked_domain("evil.example", Some("spam")) - .await - .unwrap(); - p.remove_blocked_domain("evil.example").await.unwrap(); - } - - #[tokio::test] - async fn noop_backfill_returns_empty_and_ok() { - let p = NoopApBackfill; - assert!(p.get_following(uuid::Uuid::nil()).await.unwrap().is_empty()); - p.import_remote_outbox("https://a/outbox", "https://a") - .await - .unwrap(); - p.run_backfill_for_follower(uuid::Uuid::nil(), "https://a/inbox".into()) - .await - .unwrap(); - } -} +#[path = "tests/federation.rs"] +mod tests; diff --git a/crates/domain/src/ports/rss.rs b/crates/domain/src/ports/rss.rs index 3062a934..1e4a37f0 100644 --- a/crates/domain/src/ports/rss.rs +++ b/crates/domain/src/ports/rss.rs @@ -1,3 +1,4 @@ +use crate::errors::DomainError; use crate::models::DiaryEntry; /// Channel-level metadata for a rendered feed. Paths are site-relative @@ -15,5 +16,5 @@ pub trait RssFeedRenderer: Send + Sync { &self, entries: &[DiaryEntry], channel: &FeedChannel<'_>, - ) -> Result; + ) -> Result; } diff --git a/crates/domain/src/ports/tests/federation.rs b/crates/domain/src/ports/tests/federation.rs new file mode 100644 index 00000000..052cf14b --- /dev/null +++ b/crates/domain/src/ports/tests/federation.rs @@ -0,0 +1,42 @@ +use super::*; +use crate::ports::noop::{NoopApBackfill, NoopApDocument, NoopInstanceBlocklist}; + +#[tokio::test] +async fn noop_document_returns_empty_strings() { + let p = NoopApDocument; + assert_eq!(p.actor_json("anything").await.unwrap(), ""); + assert_eq!( + p.followers_collection_json(uuid::Uuid::nil(), None) + .await + .unwrap(), + "" + ); + assert_eq!( + p.following_collection_json(uuid::Uuid::nil(), Some(2)) + .await + .unwrap(), + "" + ); +} + +#[tokio::test] +async fn noop_blocklist_returns_empty_and_ok() { + let p = NoopInstanceBlocklist; + assert!(p.get_blocked_domains().await.unwrap().is_empty()); + p.add_blocked_domain("evil.example", Some("spam")) + .await + .unwrap(); + p.remove_blocked_domain("evil.example").await.unwrap(); +} + +#[tokio::test] +async fn noop_backfill_returns_empty_and_ok() { + let p = NoopApBackfill; + assert!(p.get_following(uuid::Uuid::nil()).await.unwrap().is_empty()); + p.import_remote_outbox("https://a/outbox", "https://a") + .await + .unwrap(); + p.run_backfill_for_follower(uuid::Uuid::nil(), "https://a/inbox".into()) + .await + .unwrap(); +} diff --git a/crates/presentation/src/handlers/rss.rs b/crates/presentation/src/handlers/rss.rs index ed4d13e4..847b73ff 100644 --- a/crates/presentation/src/handlers/rss.rs +++ b/crates/presentation/src/handlers/rss.rs @@ -6,7 +6,7 @@ use axum::{ use uuid::Uuid; use application::{diary::get_diary, diary::get_user_feed, diary::queries::GetDiaryQuery}; -use domain::{errors::DomainError, models::ReviewSortBy, ports::FeedChannel}; +use domain::{models::ReviewSortBy, ports::FeedChannel}; use crate::{errors::ApiError, state::AppState}; @@ -29,7 +29,7 @@ pub async fn get_feed(State(state): State) -> Result SocialActor { - SocialActor { - identity: SocialIdentity::Local(uid.clone()), - handle: "@gabriel@md.example".into(), - display_name: None, - avatar_url: None, - } - } - - #[test] - fn local_actor_dto_url_round_trips_back_to_the_same_identity() { - let instance = InstanceIdentity::new("https://md.example"); - let uid = UserId::from_uuid(uuid::Uuid::new_v4()); - - let dto = social_actor_to_dto(local_actor(&uid), &instance); - - assert!(!dto.url.starts_with("local:"), "url was {}", dto.url); - assert_eq!( - instance.identify(&dto.url), - SocialIdentity::Local(uid), - "a local actor's dto url must parse back as Local" - ); - } - - #[cfg(feature = "federation")] - #[test] - fn local_blocked_dto_url_round_trips_back_to_the_same_identity() { - let instance = InstanceIdentity::new("https://md.example"); - let uid = UserId::from_uuid(uuid::Uuid::new_v4()); - - let dto = social_actor_to_blocked_dto(local_actor(&uid), &instance); - - assert!(!dto.url.starts_with("local:"), "url was {}", dto.url); - assert_eq!(instance.identify(&dto.url), SocialIdentity::Local(uid)); - } - - #[test] - fn local_template_url_round_trips_back_to_the_same_identity() { - let instance = InstanceIdentity::new("https://md.example"); - let uid = UserId::from_uuid(uuid::Uuid::new_v4()); - - let data = social_actor_to_template(local_actor(&uid), &instance); - - assert!(!data.url.starts_with("local:"), "url was {}", data.url); - assert_eq!(instance.identify(&data.url), SocialIdentity::Local(uid)); - } - - #[test] - fn remote_actor_dto_url_is_passed_through_unchanged() { - let instance = InstanceIdentity::new("https://md.example"); - let actor = SocialActor { - identity: SocialIdentity::Remote { - actor_url: "https://other.example/users/bob".into(), - }, - handle: "@bob@other.example".into(), - display_name: None, - avatar_url: None, - }; - - let dto = social_actor_to_dto(actor, &instance); - - assert_eq!(dto.url, "https://other.example/users/bob"); - } - - #[test] - fn local_actor_dto_carries_user_id_and_avatar() { - let instance = InstanceIdentity::new("https://md.example"); - let uid = UserId::from_uuid(uuid::Uuid::new_v4()); - let mut actor = local_actor(&uid); - actor.avatar_url = Some("https://md.example/images/a.webp".into()); - - let dto = social_actor_to_dto(actor, &instance); - - assert_eq!( - dto.user_id, - Some(uid.value()), - "local actors must carry user_id for internal links" - ); - assert_eq!( - dto.avatar_url.as_deref(), - Some("https://md.example/images/a.webp") - ); - } - - #[test] - fn remote_actor_dto_has_no_user_id() { - let instance = InstanceIdentity::new("https://md.example"); - let actor = SocialActor { - identity: SocialIdentity::Remote { - actor_url: "https://other.example/users/bob".into(), - }, - handle: "@bob@other.example".into(), - display_name: None, - avatar_url: None, - }; - assert_eq!(social_actor_to_dto(actor, &instance).user_id, None); - } -} +#[path = "tests/social.rs"] +mod tests; diff --git a/crates/presentation/src/handlers/tests/social.rs b/crates/presentation/src/handlers/tests/social.rs new file mode 100644 index 00000000..5c4999bf --- /dev/null +++ b/crates/presentation/src/handlers/tests/social.rs @@ -0,0 +1,100 @@ +use super::*; +use domain::value_objects::{InstanceIdentity, SocialActor, SocialIdentity, UserId}; + +fn local_actor(uid: &UserId) -> SocialActor { + SocialActor { + identity: SocialIdentity::Local(uid.clone()), + handle: "@gabriel@md.example".into(), + display_name: None, + avatar_url: None, + } +} + +#[test] +fn local_actor_dto_url_round_trips_back_to_the_same_identity() { + let instance = InstanceIdentity::new("https://md.example"); + let uid = UserId::from_uuid(uuid::Uuid::new_v4()); + + let dto = social_actor_to_dto(local_actor(&uid), &instance); + + assert!(!dto.url.starts_with("local:"), "url was {}", dto.url); + assert_eq!( + instance.identify(&dto.url), + SocialIdentity::Local(uid), + "a local actor's dto url must parse back as Local" + ); +} + +#[cfg(feature = "federation")] +#[test] +fn local_blocked_dto_url_round_trips_back_to_the_same_identity() { + let instance = InstanceIdentity::new("https://md.example"); + let uid = UserId::from_uuid(uuid::Uuid::new_v4()); + + let dto = social_actor_to_blocked_dto(local_actor(&uid), &instance); + + assert!(!dto.url.starts_with("local:"), "url was {}", dto.url); + assert_eq!(instance.identify(&dto.url), SocialIdentity::Local(uid)); +} + +#[test] +fn local_template_url_round_trips_back_to_the_same_identity() { + let instance = InstanceIdentity::new("https://md.example"); + let uid = UserId::from_uuid(uuid::Uuid::new_v4()); + + let data = social_actor_to_template(local_actor(&uid), &instance); + + assert!(!data.url.starts_with("local:"), "url was {}", data.url); + assert_eq!(instance.identify(&data.url), SocialIdentity::Local(uid)); +} + +#[test] +fn remote_actor_dto_url_is_passed_through_unchanged() { + let instance = InstanceIdentity::new("https://md.example"); + let actor = SocialActor { + identity: SocialIdentity::Remote { + actor_url: "https://other.example/users/bob".into(), + }, + handle: "@bob@other.example".into(), + display_name: None, + avatar_url: None, + }; + + let dto = social_actor_to_dto(actor, &instance); + + assert_eq!(dto.url, "https://other.example/users/bob"); +} + +#[test] +fn local_actor_dto_carries_user_id_and_avatar() { + let instance = InstanceIdentity::new("https://md.example"); + let uid = UserId::from_uuid(uuid::Uuid::new_v4()); + let mut actor = local_actor(&uid); + actor.avatar_url = Some("https://md.example/images/a.webp".into()); + + let dto = social_actor_to_dto(actor, &instance); + + assert_eq!( + dto.user_id, + Some(uid.value()), + "local actors must carry user_id for internal links" + ); + assert_eq!( + dto.avatar_url.as_deref(), + Some("https://md.example/images/a.webp") + ); +} + +#[test] +fn remote_actor_dto_has_no_user_id() { + let instance = InstanceIdentity::new("https://md.example"); + let actor = SocialActor { + identity: SocialIdentity::Remote { + actor_url: "https://other.example/users/bob".into(), + }, + handle: "@bob@other.example".into(), + display_name: None, + avatar_url: None, + }; + assert_eq!(social_actor_to_dto(actor, &instance).user_id, None); +} diff --git a/crates/presentation/src/tests/extractors.rs b/crates/presentation/src/tests/extractors.rs index 8a2cd575..58ae5c3a 100644 --- a/crates/presentation/src/tests/extractors.rs +++ b/crates/presentation/src/tests/extractors.rs @@ -420,7 +420,7 @@ impl domain::ports::RssFeedRenderer for Panic { &self, _: &[DiaryEntry], _: &domain::ports::FeedChannel<'_>, - ) -> Result { + ) -> Result { panic!() } }