HIGH: fix watch_medium data-loss bug, standardize error handling on ApiError, fix dep direction (rss/template-askama no longer dep on application), extract ImageFetcher port (remove reqwest from app layer), move event construction from save_review to use case, extract infra-wiring crate (DbPool/EventBusBackend dedup), deduplicate presentation helpers (encode_error, export streaming, multipart parsing) MEDIUM: split LocalApContentQuery god-trait 10→3 methods, dedup movie resolution orchestration, add RemoteActorDto/PersonDto mappers, move AppConfig to infra-wiring, fix SocialQueryPort Uuid→UserId, replace stringly-typed api-types with domain enums, move count_reviews_in_year to StatsRepository, dedup event publisher cfg blocks, extract should_enrich, move group_by_month to application, dedup count_local_posts, add FederationFlags Default, TUI input helper + ShowError rename + typed auth errors, api-types cleanup (UserSettingsDto/UserProfileBase/PreviewRowData) 102 files changed, -681 lines net
170 lines
4.5 KiB
Rust
170 lines
4.5 KiB
Rust
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
use domain::errors::DomainError;
|
|
use domain::testing::{FakeDiaryRepository, NoopSocialQueryPort};
|
|
|
|
use crate::{
|
|
config::AppConfig, diary::deps::GetActivityFeedDeps, diary::get_activity_feed,
|
|
diary::queries::GetActivityFeedQuery, test_helpers::TestContextBuilder,
|
|
};
|
|
|
|
fn default_deps() -> GetActivityFeedDeps {
|
|
GetActivityFeedDeps {
|
|
diary: FakeDiaryRepository::new() as _,
|
|
social_query: Arc::new(NoopSocialQueryPort),
|
|
config: TestContextBuilder::new().config,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn returns_empty_feed() {
|
|
let deps = default_deps();
|
|
|
|
let result = get_activity_feed::execute(
|
|
&deps,
|
|
GetActivityFeedQuery {
|
|
limit: 10,
|
|
offset: 0,
|
|
sort_by: domain::models::FeedSortBy::Date,
|
|
search: None,
|
|
viewer_user_id: None,
|
|
filter_following: false,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.items.is_empty());
|
|
assert_eq!(result.total_count, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn returns_feed_with_following_filter() {
|
|
let deps = default_deps();
|
|
|
|
let viewer = uuid::Uuid::new_v4();
|
|
|
|
let result = get_activity_feed::execute(
|
|
&deps,
|
|
GetActivityFeedQuery {
|
|
limit: 10,
|
|
offset: 0,
|
|
sort_by: domain::models::FeedSortBy::Date,
|
|
search: None,
|
|
viewer_user_id: Some(viewer),
|
|
filter_following: true,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// NoopSocialQueryPort returns empty following, so FollowingFilter
|
|
// contains only the viewer's id. Feed is empty but the code path is hit.
|
|
assert!(result.items.is_empty());
|
|
}
|
|
|
|
struct FakeSocialWithFollowing(Vec<String>);
|
|
|
|
#[async_trait]
|
|
impl domain::ports::SocialQueryPort for FakeSocialWithFollowing {
|
|
async fn get_accepted_following_urls(
|
|
&self,
|
|
_: &domain::value_objects::UserId,
|
|
) -> Result<Vec<String>, DomainError> {
|
|
Ok(self.0.clone())
|
|
}
|
|
async fn count_following(
|
|
&self,
|
|
_: &domain::value_objects::UserId,
|
|
) -> Result<usize, DomainError> {
|
|
Ok(0)
|
|
}
|
|
async fn count_accepted_followers(
|
|
&self,
|
|
_: &domain::value_objects::UserId,
|
|
) -> Result<usize, DomainError> {
|
|
Ok(0)
|
|
}
|
|
async fn get_pending_followers(
|
|
&self,
|
|
_: &domain::value_objects::UserId,
|
|
) -> Result<Vec<domain::models::PendingFollowerInfo>, DomainError> {
|
|
Ok(vec![])
|
|
}
|
|
async fn list_all_followed_remote_actors(
|
|
&self,
|
|
) -> Result<Vec<domain::models::RemoteActorInfo>, DomainError> {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn following_filter_parses_local_and_remote_urls() {
|
|
let viewer = uuid::Uuid::new_v4();
|
|
let local_friend = uuid::Uuid::new_v4();
|
|
|
|
let following_urls = vec![
|
|
format!("http://localhost:3000/users/{}", local_friend),
|
|
"https://remote.example/actor/1".to_string(),
|
|
];
|
|
|
|
let social = Arc::new(FakeSocialWithFollowing(following_urls));
|
|
|
|
let deps = GetActivityFeedDeps {
|
|
diary: FakeDiaryRepository::new() as _,
|
|
social_query: social as _,
|
|
config: AppConfig {
|
|
allow_registration: true,
|
|
base_url: "http://localhost:3000".into(),
|
|
rate_limit: 20,
|
|
refresh_ttl_seconds: 2_592_000,
|
|
wrapup: crate::config::WrapUpConfig {
|
|
font_path: None,
|
|
logo_path: None,
|
|
bg_dir: None,
|
|
},
|
|
},
|
|
};
|
|
|
|
let result = get_activity_feed::execute(
|
|
&deps,
|
|
GetActivityFeedQuery {
|
|
limit: 10,
|
|
offset: 0,
|
|
sort_by: domain::models::FeedSortBy::Date,
|
|
search: None,
|
|
viewer_user_id: Some(viewer),
|
|
filter_following: true,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Feed is empty (no data seeded), but the build_following_filter code path
|
|
// with actual URL parsing ran without errors.
|
|
assert!(result.items.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn following_filter_without_viewer_returns_none() {
|
|
let deps = default_deps();
|
|
|
|
let result = get_activity_feed::execute(
|
|
&deps,
|
|
GetActivityFeedQuery {
|
|
limit: 10,
|
|
offset: 0,
|
|
sort_by: domain::models::FeedSortBy::Date,
|
|
search: None,
|
|
viewer_user_id: None,
|
|
filter_following: true,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// filter_following=true but viewer_user_id=None → build_following_filter returns None
|
|
assert!(result.items.is_empty());
|
|
}
|