From 5118bf4700dac318efc8aed305c6841ecafe0bbe Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 25 Oct 2026 22:41:26 +0100 Subject: [PATCH] test: move activitypub tests to tests/ Assert via serialized JSON (fields pub(crate)); normalize_hashtag covered via review tags. No inline exceptions. --- crates/adapters/activitypub/src/objects.rs | 4 - .../adapters/activitypub/src/tests/objects.rs | 175 ------------------ crates/adapters/activitypub/tests/objects.rs | 4 + .../tests/objects/review_object_test.rs | 112 +++++++++++ .../tests/objects/watchlist_object_test.rs | 26 +++ 5 files changed, 142 insertions(+), 179 deletions(-) delete mode 100644 crates/adapters/activitypub/src/tests/objects.rs create mode 100644 crates/adapters/activitypub/tests/objects.rs create mode 100644 crates/adapters/activitypub/tests/objects/review_object_test.rs create mode 100644 crates/adapters/activitypub/tests/objects/watchlist_object_test.rs diff --git a/crates/adapters/activitypub/src/objects.rs b/crates/adapters/activitypub/src/objects.rs index ea7a7445..7efddc63 100644 --- a/crates/adapters/activitypub/src/objects.rs +++ b/crates/adapters/activitypub/src/objects.rs @@ -306,7 +306,3 @@ pub fn goal_to_ap_object( cc: vec![format!("{}/followers", actor_url)], } } - -#[cfg(test)] -#[path = "tests/objects.rs"] -mod tests; diff --git a/crates/adapters/activitypub/src/tests/objects.rs b/crates/adapters/activitypub/src/tests/objects.rs deleted file mode 100644 index 49090e43..00000000 --- a/crates/adapters/activitypub/src/tests/objects.rs +++ /dev/null @@ -1,175 +0,0 @@ -use super::*; - -#[test] -fn normalize_hashtag_strips_non_alphanumeric() { - assert_eq!(normalize_hashtag("The Dark Knight"), "TheDarkKnight"); - assert_eq!(normalize_hashtag("Schindler's List"), "SchindlersList"); - assert_eq!( - normalize_hashtag("2001: A Space Odyssey"), - "2001ASpaceOdyssey" - ); -} - -#[test] -fn review_to_ap_object_includes_two_hashtags() { - use chrono::NaiveDateTime; - use domain::{ - models::{PersistedReview, Review, ReviewSource}, - value_objects::{MovieId, Rating, ReviewId, UserId}, - }; - - let review = Review::from_persistence(PersistedReview { - id: ReviewId::generate(), - movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()), - user_id: UserId::from_uuid(uuid::Uuid::new_v4()), - rating: Rating::new(4).unwrap(), - comment: None, - watched_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap(), - created_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap(), - source: ReviewSource::Local, - watch_medium: None, - spoilers: false, - }); - let obj = review_to_ap_object( - &review, - ReviewApInput { - ap_id: "https://example.com/reviews/1".parse().unwrap(), - actor_url: "https://example.com/users/1".parse().unwrap(), - movie_title: "Dune".to_string(), - release_year: 2021, - external_metadata_id: None, - poster_url: None, - base_url: "https://example.com".to_string(), - }, - ); - assert_eq!(obj.tag.len(), 2); - let names: Vec<&str> = obj.tag.iter().map(|t| t.name.as_str()).collect(); - assert!(names.contains(&"#MoviesDiary")); - assert!(names.contains(&"#Dune")); -} - -#[test] -fn review_to_ap_object_has_public_addressing() { - use chrono::NaiveDateTime; - use domain::{ - models::{PersistedReview, Review, ReviewSource}, - value_objects::{MovieId, Rating, ReviewId, UserId}, - }; - - let review = Review::from_persistence(PersistedReview { - id: ReviewId::generate(), - movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()), - user_id: UserId::from_uuid(uuid::Uuid::new_v4()), - rating: Rating::new(3).unwrap(), - comment: None, - watched_at: NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap(), - created_at: NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap(), - source: ReviewSource::Local, - watch_medium: None, - spoilers: false, - }); - let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap(); - let obj = review_to_ap_object( - &review, - ReviewApInput { - ap_id: "https://example.com/reviews/1".parse().unwrap(), - actor_url: actor_url.clone(), - movie_title: "Dune".to_string(), - release_year: 2021, - external_metadata_id: None, - poster_url: None, - base_url: "https://example.com".to_string(), - }, - ); - assert_eq!(obj.to, vec!["https://www.w3.org/ns/activitystreams#Public"]); - assert_eq!(obj.cc, vec!["https://example.com/users/abc/followers"]); -} - -#[test] -fn review_to_ap_object_carries_spoiler_flag_and_summary() { - use chrono::NaiveDateTime; - use domain::{ - models::{PersistedReview, Review, ReviewSource}, - value_objects::{Comment, MovieId, Rating, ReviewId, UserId}, - }; - - let ts = NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); - let review = Review::from_persistence(PersistedReview { - id: ReviewId::generate(), - movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()), - user_id: UserId::from_uuid(uuid::Uuid::new_v4()), - rating: Rating::new(4).unwrap(), - comment: Some(Comment::new("the mentor is the villain".into()).unwrap()), - watched_at: ts, - created_at: ts, - source: ReviewSource::Local, - watch_medium: None, - spoilers: true, - }); - let obj = review_to_ap_object( - &review, - ReviewApInput { - ap_id: "https://example.com/reviews/1".parse().unwrap(), - actor_url: "https://example.com/users/1".parse().unwrap(), - movie_title: "Dune".to_string(), - release_year: 2021, - external_metadata_id: None, - poster_url: None, - base_url: "https://example.com".to_string(), - }, - ); - assert!(obj.contains_spoilers); - assert_eq!(obj.summary.as_deref(), Some("Contains spoilers")); - let json = serde_json::to_value(&obj).unwrap(); - assert_eq!(json["containsSpoilers"], serde_json::Value::Bool(true)); - assert_eq!(json["summary"], "Contains spoilers"); - - // A flag without a comment never travels: there is nothing to veil. - let review_no_comment = Review::from_persistence(PersistedReview { - id: ReviewId::generate(), - movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()), - user_id: UserId::from_uuid(uuid::Uuid::new_v4()), - rating: Rating::new(4).unwrap(), - comment: None, - watched_at: ts, - created_at: ts, - source: ReviewSource::Local, - watch_medium: None, - spoilers: true, - }); - let obj2 = review_to_ap_object( - &review_no_comment, - ReviewApInput { - ap_id: "https://example.com/reviews/2".parse().unwrap(), - actor_url: "https://example.com/users/1".parse().unwrap(), - movie_title: "Dune".to_string(), - release_year: 2021, - external_metadata_id: None, - poster_url: None, - base_url: "https://example.com".to_string(), - }, - ); - assert!(!obj2.contains_spoilers); - assert!(obj2.summary.is_none()); -} - -#[test] -fn watchlist_to_ap_object_has_public_addressing() { - let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap(); - let obj = watchlist_to_ap_object(WatchlistApInput { - ap_id: "https://example.com/watchlist/1".parse().unwrap(), - actor_url: actor_url.clone(), - movie_title: "Alien".to_string(), - release_year: 1979, - external_metadata_id: None, - poster_url: None, - added_at: chrono::Utc::now(), - base_url: "https://example.com".to_string(), - }); - assert_eq!(obj.to, vec!["https://www.w3.org/ns/activitystreams#Public"]); - assert_eq!(obj.cc, vec!["https://example.com/users/abc/followers"]); -} diff --git a/crates/adapters/activitypub/tests/objects.rs b/crates/adapters/activitypub/tests/objects.rs new file mode 100644 index 00000000..5e9a6e49 --- /dev/null +++ b/crates/adapters/activitypub/tests/objects.rs @@ -0,0 +1,4 @@ +#[path = "objects/review_object_test.rs"] +mod review_object_test; +#[path = "objects/watchlist_object_test.rs"] +mod watchlist_object_test; diff --git a/crates/adapters/activitypub/tests/objects/review_object_test.rs b/crates/adapters/activitypub/tests/objects/review_object_test.rs new file mode 100644 index 00000000..5d0d0932 --- /dev/null +++ b/crates/adapters/activitypub/tests/objects/review_object_test.rs @@ -0,0 +1,112 @@ +use activitypub::objects::{ReviewApInput, review_to_ap_object}; +use chrono::NaiveDateTime; +use domain::models::{PersistedReview, Review, ReviewSource}; +use domain::value_objects::{Comment, MovieId, Rating, ReviewId, UserId}; +use serde_json::{Value, json}; + +const PUBLIC: &str = "https://www.w3.org/ns/activitystreams#Public"; + +fn timestamp() -> NaiveDateTime { + NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap() +} + +fn review(rating: u8, comment: Option<&str>, spoilers: bool) -> Review { + Review::from_persistence(PersistedReview { + id: ReviewId::generate(), + movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()), + user_id: UserId::from_uuid(uuid::Uuid::new_v4()), + rating: Rating::new(rating).unwrap(), + comment: comment.map(|c| Comment::new(c.into()).unwrap()), + watched_at: timestamp(), + created_at: timestamp(), + source: ReviewSource::Local, + watch_medium: None, + spoilers, + }) +} + +fn input(movie_title: &str, actor_url: &str) -> ReviewApInput { + ReviewApInput { + ap_id: "https://example.com/reviews/1".parse().unwrap(), + actor_url: actor_url.parse().unwrap(), + movie_title: movie_title.to_string(), + release_year: 2021, + external_metadata_id: None, + poster_url: None, + base_url: "https://example.com".to_string(), + } +} + +fn render(review: &Review, input: ReviewApInput) -> Value { + serde_json::to_value(review_to_ap_object(review, input)).unwrap() +} + +fn tag_names(json: &Value) -> Vec<&str> { + json["tag"] + .as_array() + .unwrap() + .iter() + .map(|t| t["name"].as_str().unwrap()) + .collect() +} + +#[test] +fn review_object_tags_movie_title_with_non_alphanumerics_stripped() { + for (title, hashtag) in [ + ("The Dark Knight", "#TheDarkKnight"), + ("Schindler's List", "#SchindlersList"), + ("2001: A Space Odyssey", "#2001ASpaceOdyssey"), + ] { + let json = render( + &review(4, None, false), + input(title, "https://example.com/users/1"), + ); + assert!( + tag_names(&json).contains(&hashtag), + "{title}: {:?}", + tag_names(&json) + ); + } +} + +#[test] +fn review_object_includes_two_hashtags() { + let json = render( + &review(4, None, false), + input("Dune", "https://example.com/users/1"), + ); + + assert_eq!(tag_names(&json), vec!["#MoviesDiary", "#Dune"]); +} + +#[test] +fn review_object_is_publicly_addressed_with_followers_in_cc() { + let json = render( + &review(3, None, false), + input("Dune", "https://example.com/users/abc"), + ); + + assert_eq!(json["to"], json!([PUBLIC])); + assert_eq!( + json["cc"], + json!(["https://example.com/users/abc/followers"]) + ); +} + +#[test] +fn review_object_carries_spoiler_flag_and_summary_only_with_a_comment() { + let json = render( + &review(4, Some("the mentor is the villain"), true), + input("Dune", "https://example.com/users/1"), + ); + assert_eq!(json["containsSpoilers"], Value::Bool(true)); + assert_eq!(json["summary"], "Contains spoilers"); + + // A flag without a comment never travels: there is nothing to veil. + let json = render( + &review(4, None, true), + input("Dune", "https://example.com/users/1"), + ); + assert!(json.get("containsSpoilers").is_none()); + assert!(json.get("summary").is_none()); +} diff --git a/crates/adapters/activitypub/tests/objects/watchlist_object_test.rs b/crates/adapters/activitypub/tests/objects/watchlist_object_test.rs new file mode 100644 index 00000000..a4fa5960 --- /dev/null +++ b/crates/adapters/activitypub/tests/objects/watchlist_object_test.rs @@ -0,0 +1,26 @@ +use activitypub::objects::{WatchlistApInput, watchlist_to_ap_object}; +use serde_json::json; + +#[test] +fn watchlist_object_is_publicly_addressed_with_followers_in_cc() { + let obj = watchlist_to_ap_object(WatchlistApInput { + ap_id: "https://example.com/watchlist/1".parse().unwrap(), + actor_url: "https://example.com/users/abc".parse().unwrap(), + movie_title: "Alien".to_string(), + release_year: 1979, + external_metadata_id: None, + poster_url: None, + added_at: chrono::Utc::now(), + base_url: "https://example.com".to_string(), + }); + let json = serde_json::to_value(&obj).unwrap(); + + assert_eq!( + json["to"], + json!(["https://www.w3.org/ns/activitystreams#Public"]) + ); + assert_eq!( + json["cc"], + json!(["https://example.com/users/abc/followers"]) + ); +}