From d4da172398a89a9cccb9a2d0ab67e2f10ddf5dbd Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 28 May 2026 03:44:41 +0200 Subject: [PATCH] feat(application): add get_remote_friends use case --- .../use_cases/federation_management/mod.rs | 14 +++++ .../use_cases/federation_management/tests.rs | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/crates/application/src/use_cases/federation_management/mod.rs b/crates/application/src/use_cases/federation_management/mod.rs index 88b5acd..50c714e 100644 --- a/crates/application/src/use_cases/federation_management/mod.rs +++ b/crates/application/src/use_cases/federation_management/mod.rs @@ -96,6 +96,20 @@ pub async fn list_remote_following( federation.get_remote_following(user_id).await } +pub async fn get_remote_friends( + federation: &dyn FederationActionPort, + user_id: &UserId, +) -> Result, DomainError> { + use std::collections::HashSet; + let following = federation.get_remote_following(user_id).await?; + let followers = federation.get_remote_followers(user_id).await?; + let follower_urls: HashSet<&str> = followers.iter().map(|a| a.url.as_str()).collect(); + Ok(following + .into_iter() + .filter(|a| follower_urls.contains(a.url.as_str())) + .collect()) +} + pub async fn remove_remote_following( follows: &dyn FollowRepository, users: &dyn UserReader, diff --git a/crates/application/src/use_cases/federation_management/tests.rs b/crates/application/src/use_cases/federation_management/tests.rs index 664f3e1..76c8207 100644 --- a/crates/application/src/use_cases/federation_management/tests.rs +++ b/crates/application/src/use_cases/federation_management/tests.rs @@ -1,6 +1,25 @@ use super::*; +use chrono::Utc; +use domain::models::remote_actor::RemoteActor; use domain::testing::TestStore; +fn remote_actor(url: &str, handle: &str) -> RemoteActor { + RemoteActor { + url: url.to_string(), + handle: handle.to_string(), + display_name: None, + avatar_url: None, + bio: None, + banner_url: None, + also_known_as: None, + outbox_url: None, + followers_url: None, + following_url: None, + attachment: vec![], + last_fetched_at: Utc::now(), + } +} + #[tokio::test] async fn list_pending_returns_empty_by_default() { let store = TestStore::default(); @@ -51,3 +70,41 @@ async fn list_remote_following_returns_empty_by_default() { let result = list_remote_following(&store, &uid).await.unwrap(); assert!(result.is_empty()); } + +#[tokio::test] +async fn get_remote_friends_returns_intersection() { + let store = TestStore::default(); + let uid = UserId::new(); + + let bob = remote_actor("https://bob.example.com/users/bob", "bob@bob.example.com"); + let carol = remote_actor( + "https://carol.example.com/users/carol", + "carol@carol.example.com", + ); + + // uid follows bob and carol + store + .remote_following + .lock() + .unwrap() + .extend([bob.clone(), carol.clone()]); + // only bob follows back + store.remote_followers.lock().unwrap().push(bob.clone()); + + let friends = get_remote_friends(&store, &uid).await.unwrap(); + assert_eq!(friends.len(), 1); + assert_eq!(friends[0].url, bob.url); +} + +#[tokio::test] +async fn get_remote_friends_empty_when_no_mutual() { + let store = TestStore::default(); + let uid = UserId::new(); + + let bob = remote_actor("https://bob.example.com/users/bob", "bob@bob.example.com"); + store.remote_following.lock().unwrap().push(bob.clone()); + // bob does NOT follow back + + let friends = get_remote_friends(&store, &uid).await.unwrap(); + assert!(friends.is_empty()); +}