use std::sync::Arc;
use super::*;
use crate::automation::analytics_loop::{EngagementFetcher, ProfileFetcher};
use crate::automation::loop_helpers::{LoopError, MentionsFetcher, ThreadPoster, TweetSearcher};
use crate::automation::posting_queue::PostExecutor;
use crate::automation::target_loop::{TargetTweetFetcher, TargetUserManager};
use crate::x_api::types::*;
use crate::x_api::{SearchResponse, XApiClient};
struct MockXApiClient;
#[async_trait::async_trait]
impl XApiClient for MockXApiClient {
async fn search_tweets(
&self,
query: &str,
_: u32,
_: Option<&str>,
_: Option<&str>,
) -> Result<SearchResponse, crate::error::XApiError> {
Ok(SearchResponse {
data: vec![Tweet {
id: "st1".into(),
text: query.into(),
author_id: "a1".into(),
created_at: String::new(),
public_metrics: PublicMetrics::default(),
conversation_id: None,
}],
includes: None,
meta: SearchMeta {
newest_id: None,
oldest_id: None,
result_count: 1,
next_token: None,
},
})
}
async fn get_mentions(
&self,
_: &str,
_: Option<&str>,
_: Option<&str>,
) -> Result<MentionResponse, crate::error::XApiError> {
Ok(SearchResponse {
data: vec![Tweet {
id: "m1".into(),
text: "mention".into(),
author_id: "a2".into(),
created_at: String::new(),
public_metrics: PublicMetrics::default(),
conversation_id: None,
}],
includes: None,
meta: SearchMeta {
newest_id: None,
oldest_id: None,
result_count: 1,
next_token: None,
},
})
}
async fn post_tweet(&self, text: &str) -> Result<PostedTweet, crate::error::XApiError> {
Ok(PostedTweet {
id: "pt1".into(),
text: text.into(),
})
}
async fn reply_to_tweet(
&self,
text: &str,
_: &str,
) -> Result<PostedTweet, crate::error::XApiError> {
Ok(PostedTweet {
id: "rt1".into(),
text: text.into(),
})
}
async fn get_tweet(&self, id: &str) -> Result<Tweet, crate::error::XApiError> {
Ok(Tweet {
id: id.into(),
text: "tweet text".into(),
author_id: "a1".into(),
created_at: String::new(),
public_metrics: PublicMetrics::default(),
conversation_id: None,
})
}
async fn get_me(&self) -> Result<User, crate::error::XApiError> {
Ok(User {
id: "me".into(),
username: "testuser".into(),
name: "Test".into(),
profile_image_url: None,
description: None,
location: None,
url: None,
public_metrics: UserMetrics::default(),
})
}
async fn get_user_tweets(
&self,
_: &str,
_: u32,
_: Option<&str>,
) -> Result<SearchResponse, crate::error::XApiError> {
Ok(SearchResponse {
data: vec![],
includes: None,
meta: SearchMeta {
newest_id: None,
oldest_id: None,
result_count: 0,
next_token: None,
},
})
}
async fn get_user_by_username(&self, u: &str) -> Result<User, crate::error::XApiError> {
Ok(User {
id: format!("uid_{u}"),
username: u.into(),
name: "Test".into(),
profile_image_url: None,
description: None,
location: None,
url: None,
public_metrics: UserMetrics::default(),
})
}
}
fn mock_client() -> Arc<dyn XApiClient> {
Arc::new(MockXApiClient) as Arc<dyn XApiClient>
}
#[tokio::test]
async fn search_adapter_routes_through_toolkit() {
let adapter = XApiSearchAdapter::new(mock_client());
let tweets = adapter.search_tweets("rust").await.unwrap();
assert_eq!(tweets.len(), 1);
assert_eq!(tweets[0].id, "st1");
assert_eq!(tweets[0].text, "rust");
}
#[tokio::test]
async fn mentions_adapter_routes_through_toolkit() {
let adapter = XApiMentionsAdapter::new(mock_client(), "me".into());
let tweets = adapter.get_mentions(None).await.unwrap();
assert_eq!(tweets.len(), 1);
assert_eq!(tweets[0].id, "m1");
}
#[tokio::test]
async fn target_adapter_fetch_routes_through_toolkit() {
let adapter = XApiTargetAdapter::new(mock_client());
let tweets = adapter.fetch_user_tweets("u1").await.unwrap();
assert!(tweets.is_empty()); }
#[tokio::test]
async fn target_adapter_lookup_routes_through_toolkit() {
let adapter = XApiTargetAdapter::new(mock_client());
let (id, username) = adapter.lookup_user("alice").await.unwrap();
assert_eq!(id, "uid_alice");
assert_eq!(username, "alice");
}
#[tokio::test]
async fn profile_adapter_routes_through_toolkit() {
let adapter = XApiProfileAdapter::new(mock_client());
let metrics = adapter.get_profile_metrics().await.unwrap();
assert_eq!(metrics.follower_count, 0);
}
#[tokio::test]
async fn engagement_adapter_routes_through_toolkit() {
let adapter = XApiProfileAdapter::new(mock_client());
let metrics = adapter.get_tweet_metrics("t123").await.unwrap();
assert_eq!(metrics.likes, 0);
}
#[tokio::test]
async fn post_executor_reply_routes_through_toolkit() {
let adapter = XApiPostExecutorAdapter::new(mock_client());
let id = adapter.execute_reply("t0", "hello", &[]).await.unwrap();
assert_eq!(id, "rt1");
}
#[tokio::test]
async fn post_executor_tweet_routes_through_toolkit() {
let adapter = XApiPostExecutorAdapter::new(mock_client());
let id = adapter.execute_tweet("hello", &[]).await.unwrap();
assert_eq!(id, "pt1");
}
#[tokio::test]
async fn thread_poster_post_routes_through_toolkit() {
let adapter = XApiThreadPosterAdapter::new(mock_client());
let id = adapter.post_tweet("thread start").await.unwrap();
assert_eq!(id, "pt1");
}
#[tokio::test]
async fn thread_poster_reply_routes_through_toolkit() {
let adapter = XApiThreadPosterAdapter::new(mock_client());
let id = adapter.reply_to_tweet("t0", "thread cont").await.unwrap();
assert_eq!(id, "rt1");
}
#[tokio::test]
async fn toolkit_error_maps_to_loop_error() {
struct FailClient;
#[async_trait::async_trait]
impl XApiClient for FailClient {
async fn search_tweets(
&self,
_: &str,
_: u32,
_: Option<&str>,
_: Option<&str>,
) -> Result<SearchResponse, crate::error::XApiError> {
Err(crate::error::XApiError::RateLimited {
retry_after: Some(30),
})
}
async fn get_mentions(
&self,
_: &str,
_: Option<&str>,
_: Option<&str>,
) -> Result<MentionResponse, crate::error::XApiError> {
unimplemented!()
}
async fn post_tweet(&self, _: &str) -> Result<PostedTweet, crate::error::XApiError> {
unimplemented!()
}
async fn reply_to_tweet(
&self,
_: &str,
_: &str,
) -> Result<PostedTweet, crate::error::XApiError> {
unimplemented!()
}
async fn get_tweet(&self, _: &str) -> Result<Tweet, crate::error::XApiError> {
unimplemented!()
}
async fn get_me(&self) -> Result<User, crate::error::XApiError> {
unimplemented!()
}
async fn get_user_tweets(
&self,
_: &str,
_: u32,
_: Option<&str>,
) -> Result<SearchResponse, crate::error::XApiError> {
unimplemented!()
}
async fn get_user_by_username(&self, _: &str) -> Result<User, crate::error::XApiError> {
unimplemented!()
}
}
let client: Arc<dyn XApiClient> = Arc::new(FailClient);
let adapter = XApiSearchAdapter::new(client);
let err = adapter.search_tweets("q").await.unwrap_err();
assert!(matches!(
err,
LoopError::RateLimited {
retry_after: Some(30)
}
));
}
#[tokio::test]
async fn empty_id_triggers_toolkit_validation() {
let adapter = XApiTargetAdapter::new(mock_client());
let err = adapter.fetch_user_tweets("").await.unwrap_err();
assert!(matches!(err, LoopError::Other(_)));
}