refactor: implement trait-based DI for TdClient and fix stack overflow

Implement complete trait-based dependency injection pattern for TdClient
to enable testing with FakeTdClient mock. Fix critical stack overflow bugs
caused by infinite recursion in trait implementations.

Breaking Changes:
- App is now generic: App<T: TdClientTrait = TdClient>
- All UI and input handlers are generic over TdClientTrait
- TdClient methods now accessed through trait interface

New Files:
- src/tdlib/trait.rs: TdClientTrait definition with 40+ methods
- src/tdlib/client_impl.rs: TdClientTrait impl for TdClient
- tests/helpers/fake_tdclient_impl.rs: TdClientTrait impl for FakeTdClient

Critical Fixes:
- Fix stack overflow in send_message, edit_message, delete_messages
- Fix stack overflow in forward_messages, current_chat_messages
- Fix stack overflow in current_pinned_message
- All methods now call message_manager directly to avoid recursion

Testing:
- FakeTdClient supports configurable auth_state for auth screen tests
- Added pinned message support in FakeTdClient
- All 196+ tests passing (188 tests + 8 benchmarks)

Dependencies:
- Added async-trait = "0.1"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-02 05:42:19 +03:00
parent ed5a4f9c72
commit 8e48d076de
38 changed files with 1053 additions and 161 deletions

125
src/tdlib/trait.rs Normal file
View File

@@ -0,0 +1,125 @@
//! Trait definition for TdClient to enable dependency injection
//!
//! This trait allows tests to use FakeTdClient instead of real TDLib client.
use crate::tdlib::{AuthState, FolderInfo, MessageInfo, ProfileInfo, UserCache, UserOnlineStatus};
use crate::types::{ChatId, MessageId, UserId};
use async_trait::async_trait;
use tdlib_rs::enums::{ChatAction, Update};
use super::ChatInfo;
/// Trait for TDLib client operations
///
/// This trait defines the interface for both real and fake TDLib clients,
/// enabling dependency injection and easier testing.
#[async_trait]
pub trait TdClientTrait: Send {
// ============ Auth methods ============
async fn send_phone_number(&self, phone: String) -> Result<(), String>;
async fn send_code(&self, code: String) -> Result<(), String>;
async fn send_password(&self, password: String) -> Result<(), String>;
// ============ Chat methods ============
async fn load_chats(&mut self, limit: i32) -> Result<(), String>;
async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String>;
async fn leave_chat(&self, chat_id: ChatId) -> Result<(), String>;
async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String>;
// ============ Chat actions ============
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction);
fn clear_stale_typing_status(&mut self) -> bool;
// ============ Message methods ============
async fn get_chat_history(&mut self, chat_id: ChatId, limit: i32) -> Result<Vec<MessageInfo>, String>;
async fn load_older_messages(&mut self, chat_id: ChatId, from_message_id: MessageId) -> Result<Vec<MessageInfo>, String>;
async fn get_pinned_messages(&mut self, chat_id: ChatId) -> Result<Vec<MessageInfo>, String>;
async fn load_current_pinned_message(&mut self, chat_id: ChatId);
async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result<Vec<MessageInfo>, String>;
async fn send_message(
&mut self,
chat_id: ChatId,
text: String,
reply_to_message_id: Option<MessageId>,
reply_info: Option<super::ReplyInfo>,
) -> Result<MessageInfo, String>;
async fn edit_message(
&mut self,
chat_id: ChatId,
message_id: MessageId,
new_text: String,
) -> Result<MessageInfo, String>;
async fn delete_messages(
&mut self,
chat_id: ChatId,
message_ids: Vec<MessageId>,
revoke: bool,
) -> Result<(), String>;
async fn forward_messages(
&mut self,
to_chat_id: ChatId,
from_chat_id: ChatId,
message_ids: Vec<MessageId>,
) -> Result<(), String>;
async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String>;
fn push_message(&mut self, msg: MessageInfo);
async fn fetch_missing_reply_info(&mut self);
async fn process_pending_view_messages(&mut self);
// ============ User methods ============
fn get_user_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus>;
async fn process_pending_user_ids(&mut self);
// ============ Reaction methods ============
async fn get_message_available_reactions(
&self,
chat_id: ChatId,
message_id: MessageId,
) -> Result<Vec<String>, String>;
async fn toggle_reaction(
&self,
chat_id: ChatId,
message_id: MessageId,
reaction: String,
) -> Result<(), String>;
// ============ Getters (immutable) ============
fn client_id(&self) -> i32;
async fn get_me(&self) -> Result<i64, String>;
fn auth_state(&self) -> &AuthState;
fn chats(&self) -> &[ChatInfo];
fn folders(&self) -> &[FolderInfo];
fn current_chat_messages(&self) -> Vec<MessageInfo>;
fn current_chat_id(&self) -> Option<ChatId>;
fn current_pinned_message(&self) -> Option<MessageInfo>;
fn typing_status(&self) -> Option<&(UserId, String, std::time::Instant)>;
fn pending_view_messages(&self) -> &[(ChatId, Vec<MessageId>)];
fn pending_user_ids(&self) -> &[UserId];
fn main_chat_list_position(&self) -> i32;
fn user_cache(&self) -> &UserCache;
fn network_state(&self) -> super::types::NetworkState;
// ============ Setters (mutable) ============
fn chats_mut(&mut self) -> &mut Vec<ChatInfo>;
fn folders_mut(&mut self) -> &mut Vec<FolderInfo>;
fn current_chat_messages_mut(&mut self) -> &mut Vec<MessageInfo>;
fn clear_current_chat_messages(&mut self);
fn set_current_chat_messages(&mut self, messages: Vec<MessageInfo>);
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>);
fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>);
fn set_typing_status(&mut self, status: Option<(UserId, String, std::time::Instant)>);
fn pending_view_messages_mut(&mut self) -> &mut Vec<(ChatId, Vec<MessageId>)>;
fn pending_user_ids_mut(&mut self) -> &mut Vec<UserId>;
fn set_main_chat_list_position(&mut self, position: i32);
fn user_cache_mut(&mut self) -> &mut UserCache;
// ============ Update handling ============
fn handle_update(&mut self, update: Update);
}