Skip to main content

tuitbot_core/x_api/
null_client.rs

1//! Null X API client that returns errors for all operations.
2//!
3//! Used when X API tokens are not available, allowing MCP profiles
4//! to start in degraded mode with non-X tools still functional.
5
6use crate::error::XApiError;
7use crate::x_api::types::*;
8use crate::x_api::XApiClient;
9
10const NOT_CONFIGURED: &str = "X API client not configured. Run `tuitbot auth` to authenticate.";
11
12/// A no-op X API client that returns [`XApiError::ApiError`] for every call.
13///
14/// Injected when tokens are missing or expired so that the MCP server can
15/// still start and serve config/scoring tools.
16pub struct NullXApiClient;
17
18#[async_trait::async_trait]
19impl XApiClient for NullXApiClient {
20    async fn search_tweets(
21        &self,
22        _query: &str,
23        _max_results: u32,
24        _since_id: Option<&str>,
25        _pagination_token: Option<&str>,
26    ) -> Result<SearchResponse, XApiError> {
27        Err(not_configured())
28    }
29
30    async fn get_mentions(
31        &self,
32        _user_id: &str,
33        _since_id: Option<&str>,
34        _pagination_token: Option<&str>,
35    ) -> Result<MentionResponse, XApiError> {
36        Err(not_configured())
37    }
38
39    async fn post_tweet(&self, _text: &str) -> Result<PostedTweet, XApiError> {
40        Err(not_configured())
41    }
42
43    async fn reply_to_tweet(
44        &self,
45        _text: &str,
46        _in_reply_to_id: &str,
47    ) -> Result<PostedTweet, XApiError> {
48        Err(not_configured())
49    }
50
51    async fn get_tweet(&self, _tweet_id: &str) -> Result<Tweet, XApiError> {
52        Err(not_configured())
53    }
54
55    async fn get_me(&self) -> Result<User, XApiError> {
56        Err(not_configured())
57    }
58
59    async fn get_user_tweets(
60        &self,
61        _user_id: &str,
62        _max_results: u32,
63        _pagination_token: Option<&str>,
64    ) -> Result<SearchResponse, XApiError> {
65        Err(not_configured())
66    }
67
68    async fn get_user_by_username(&self, _username: &str) -> Result<User, XApiError> {
69        Err(not_configured())
70    }
71}
72
73fn not_configured() -> XApiError {
74    XApiError::ApiError {
75        status: 0,
76        message: NOT_CONFIGURED.to_string(),
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[tokio::test]
85    async fn null_client_search_tweets() {
86        let client = NullXApiClient;
87        let result = client.search_tweets("query", 10, None, None).await;
88        assert!(result.is_err());
89        let err = result.unwrap_err();
90        assert!(format!("{err}").contains("not configured"));
91    }
92
93    #[tokio::test]
94    async fn null_client_get_mentions() {
95        let client = NullXApiClient;
96        let result = client.get_mentions("u1", None, None).await;
97        assert!(result.is_err());
98    }
99
100    #[tokio::test]
101    async fn null_client_post_tweet() {
102        let client = NullXApiClient;
103        let result = client.post_tweet("text").await;
104        assert!(result.is_err());
105    }
106
107    #[tokio::test]
108    async fn null_client_reply_to_tweet() {
109        let client = NullXApiClient;
110        let result = client.reply_to_tweet("text", "123").await;
111        assert!(result.is_err());
112    }
113
114    #[tokio::test]
115    async fn null_client_get_tweet() {
116        let client = NullXApiClient;
117        let result = client.get_tweet("123").await;
118        assert!(result.is_err());
119    }
120
121    #[tokio::test]
122    async fn null_client_get_me() {
123        let client = NullXApiClient;
124        let result = client.get_me().await;
125        assert!(result.is_err());
126    }
127
128    #[tokio::test]
129    async fn null_client_get_user_tweets() {
130        let client = NullXApiClient;
131        let result = client.get_user_tweets("u1", 10, None).await;
132        assert!(result.is_err());
133    }
134
135    #[tokio::test]
136    async fn null_client_get_user_by_username() {
137        let client = NullXApiClient;
138        let result = client.get_user_by_username("testuser").await;
139        assert!(result.is_err());
140    }
141
142    #[test]
143    fn not_configured_error_message() {
144        let err = not_configured();
145        match err {
146            XApiError::ApiError { status, message } => {
147                assert_eq!(status, 0);
148                assert!(message.contains("not configured"));
149                assert!(message.contains("tuitbot auth"));
150            }
151            _ => panic!("expected ApiError"),
152        }
153    }
154
155    // ── Default trait method coverage via NullXApiClient ──────────
156
157    #[tokio::test]
158    async fn null_client_upload_media() {
159        let client = NullXApiClient;
160        let result = client.upload_media(b"data", MediaType::Gif).await;
161        assert!(result.is_err());
162    }
163
164    #[tokio::test]
165    async fn null_client_post_tweet_with_media() {
166        let client = NullXApiClient;
167        let result = client
168            .post_tweet_with_media("text", &["m1".to_string()])
169            .await;
170        assert!(result.is_err());
171    }
172
173    #[tokio::test]
174    async fn null_client_reply_with_media() {
175        let client = NullXApiClient;
176        let result = client
177            .reply_to_tweet_with_media("text", "123", &["m1".to_string()])
178            .await;
179        assert!(result.is_err());
180    }
181
182    #[tokio::test]
183    async fn null_client_quote_tweet() {
184        let client = NullXApiClient;
185        let result = client.quote_tweet("text", "123").await;
186        assert!(result.is_err());
187    }
188
189    #[tokio::test]
190    async fn null_client_like_tweet() {
191        let client = NullXApiClient;
192        let result = client.like_tweet("u1", "t1").await;
193        assert!(result.is_err());
194    }
195
196    #[tokio::test]
197    async fn null_client_follow_user() {
198        let client = NullXApiClient;
199        let result = client.follow_user("u1", "u2").await;
200        assert!(result.is_err());
201    }
202
203    #[tokio::test]
204    async fn null_client_unfollow_user() {
205        let client = NullXApiClient;
206        let result = client.unfollow_user("u1", "u2").await;
207        assert!(result.is_err());
208    }
209
210    #[tokio::test]
211    async fn null_client_retweet() {
212        let client = NullXApiClient;
213        let result = client.retweet("u1", "t1").await;
214        assert!(result.is_err());
215    }
216
217    #[tokio::test]
218    async fn null_client_unretweet() {
219        let client = NullXApiClient;
220        let result = client.unretweet("u1", "t1").await;
221        assert!(result.is_err());
222    }
223
224    #[tokio::test]
225    async fn null_client_delete_tweet() {
226        let client = NullXApiClient;
227        let result = client.delete_tweet("t1").await;
228        assert!(result.is_err());
229    }
230
231    #[tokio::test]
232    async fn null_client_get_home_timeline() {
233        let client = NullXApiClient;
234        let result = client.get_home_timeline("u1", 10, None).await;
235        assert!(result.is_err());
236    }
237
238    #[tokio::test]
239    async fn null_client_unlike_tweet() {
240        let client = NullXApiClient;
241        let result = client.unlike_tweet("u1", "t1").await;
242        assert!(result.is_err());
243    }
244
245    #[tokio::test]
246    async fn null_client_get_followers() {
247        let client = NullXApiClient;
248        let result = client.get_followers("u1", 10, None).await;
249        assert!(result.is_err());
250    }
251
252    #[tokio::test]
253    async fn null_client_get_following() {
254        let client = NullXApiClient;
255        let result = client.get_following("u1", 10, None).await;
256        assert!(result.is_err());
257    }
258
259    #[tokio::test]
260    async fn null_client_get_user_by_id() {
261        let client = NullXApiClient;
262        let result = client.get_user_by_id("u1").await;
263        assert!(result.is_err());
264    }
265
266    #[tokio::test]
267    async fn null_client_get_liked_tweets() {
268        let client = NullXApiClient;
269        let result = client.get_liked_tweets("u1", 10, None).await;
270        assert!(result.is_err());
271    }
272
273    #[tokio::test]
274    async fn null_client_get_bookmarks() {
275        let client = NullXApiClient;
276        let result = client.get_bookmarks("u1", 10, None).await;
277        assert!(result.is_err());
278    }
279
280    #[tokio::test]
281    async fn null_client_bookmark_tweet() {
282        let client = NullXApiClient;
283        let result = client.bookmark_tweet("u1", "t1").await;
284        assert!(result.is_err());
285    }
286
287    #[tokio::test]
288    async fn null_client_unbookmark_tweet() {
289        let client = NullXApiClient;
290        let result = client.unbookmark_tweet("u1", "t1").await;
291        assert!(result.is_err());
292    }
293
294    #[tokio::test]
295    async fn null_client_get_users_by_ids() {
296        let client = NullXApiClient;
297        let result = client.get_users_by_ids(&["u1", "u2"]).await;
298        assert!(result.is_err());
299    }
300
301    #[tokio::test]
302    async fn null_client_get_tweet_liking_users() {
303        let client = NullXApiClient;
304        let result = client.get_tweet_liking_users("t1", 10, None).await;
305        assert!(result.is_err());
306    }
307
308    #[tokio::test]
309    async fn null_client_raw_request() {
310        let client = NullXApiClient;
311        let result = client.raw_request("GET", "/test", None, None, None).await;
312        assert!(result.is_err());
313    }
314
315    // ── Error message consistency ─────────────────────────────────
316
317    #[tokio::test]
318    async fn all_null_client_errors_contain_auth_hint() {
319        let client = NullXApiClient;
320        let err_msg = format!("{}", client.post_tweet("test").await.unwrap_err());
321        assert!(
322            err_msg.contains("tuitbot auth"),
323            "error should contain auth hint"
324        );
325    }
326}