Files
telegram-tui/tests/helpers/fake_tdclient.rs
Mikhail Kilin 8e48d076de 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>
2026-02-02 05:42:19 +03:00

907 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fake TDLib client for testing
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo};
use tele_tui::tdlib::types::{FolderInfo, ReactionInfo};
use tele_tui::types::{ChatId, MessageId, UserId};
use tokio::sync::mpsc;
/// Update события от TDLib (упрощённая версия)
#[derive(Debug, Clone)]
pub enum TdUpdate {
NewMessage { chat_id: ChatId, message: MessageInfo },
MessageContent { chat_id: ChatId, message_id: MessageId, new_text: String },
DeleteMessages { chat_id: ChatId, message_ids: Vec<MessageId> },
ChatAction { chat_id: ChatId, user_id: UserId, action: String },
MessageInteractionInfo { chat_id: ChatId, message_id: MessageId, reactions: Vec<ReactionInfo> },
ConnectionState { state: NetworkState },
ChatReadOutbox { chat_id: ChatId, last_read_outbox_message_id: MessageId },
ChatDraftMessage { chat_id: ChatId, draft_text: Option<String> },
}
/// Упрощённый mock TDLib клиента для тестов
pub struct FakeTdClient {
// Данные
pub chats: Arc<Mutex<Vec<ChatInfo>>>,
pub messages: Arc<Mutex<HashMap<i64, Vec<MessageInfo>>>>,
pub folders: Arc<Mutex<Vec<FolderInfo>>>,
pub user_names: Arc<Mutex<HashMap<i64, String>>>,
pub profiles: Arc<Mutex<HashMap<i64, ProfileInfo>>>,
pub drafts: Arc<Mutex<HashMap<i64, String>>>,
pub available_reactions: Arc<Mutex<Vec<String>>>,
// Состояние
pub network_state: Arc<Mutex<NetworkState>>,
pub typing_chat_id: Arc<Mutex<Option<i64>>>,
pub current_chat_id: Arc<Mutex<Option<i64>>>,
pub current_pinned_message: Arc<Mutex<Option<MessageInfo>>>,
pub auth_state: Arc<Mutex<AuthState>>,
// История действий (для проверки в тестах)
pub sent_messages: Arc<Mutex<Vec<SentMessage>>>,
pub edited_messages: Arc<Mutex<Vec<EditedMessage>>>,
pub deleted_messages: Arc<Mutex<Vec<DeletedMessages>>>,
pub forwarded_messages: Arc<Mutex<Vec<ForwardedMessages>>>,
pub searched_queries: Arc<Mutex<Vec<SearchQuery>>>,
pub viewed_messages: Arc<Mutex<Vec<(i64, Vec<i64>)>>>, // (chat_id, message_ids)
pub chat_actions: Arc<Mutex<Vec<(i64, String)>>>, // (chat_id, action)
// Update channel для симуляции событий
pub update_tx: Arc<Mutex<Option<mpsc::UnboundedSender<TdUpdate>>>>,
// Настройки поведения
pub simulate_delays: bool,
pub fail_next_operation: Arc<Mutex<bool>>,
}
#[derive(Debug, Clone)]
pub struct SentMessage {
pub chat_id: i64,
pub text: String,
pub reply_to: Option<MessageId>,
pub reply_info: Option<ReplyInfo>,
}
#[derive(Debug, Clone)]
pub struct EditedMessage {
pub chat_id: i64,
pub message_id: MessageId,
pub new_text: String,
}
#[derive(Debug, Clone)]
pub struct DeletedMessages {
pub chat_id: i64,
pub message_ids: Vec<MessageId>,
pub revoke: bool,
}
#[derive(Debug, Clone)]
pub struct ForwardedMessages {
pub from_chat_id: i64,
pub to_chat_id: i64,
pub message_ids: Vec<MessageId>,
}
#[derive(Debug, Clone)]
pub struct SearchQuery {
pub chat_id: i64,
pub query: String,
pub results_count: usize,
}
impl Default for FakeTdClient {
fn default() -> Self {
Self::new()
}
}
impl Clone for FakeTdClient {
fn clone(&self) -> Self {
Self {
chats: Arc::clone(&self.chats),
messages: Arc::clone(&self.messages),
folders: Arc::clone(&self.folders),
user_names: Arc::clone(&self.user_names),
profiles: Arc::clone(&self.profiles),
drafts: Arc::clone(&self.drafts),
available_reactions: Arc::clone(&self.available_reactions),
network_state: Arc::clone(&self.network_state),
typing_chat_id: Arc::clone(&self.typing_chat_id),
current_chat_id: Arc::clone(&self.current_chat_id),
current_pinned_message: Arc::clone(&self.current_pinned_message),
auth_state: Arc::clone(&self.auth_state),
sent_messages: Arc::clone(&self.sent_messages),
edited_messages: Arc::clone(&self.edited_messages),
deleted_messages: Arc::clone(&self.deleted_messages),
forwarded_messages: Arc::clone(&self.forwarded_messages),
searched_queries: Arc::clone(&self.searched_queries),
viewed_messages: Arc::clone(&self.viewed_messages),
chat_actions: Arc::clone(&self.chat_actions),
update_tx: Arc::clone(&self.update_tx),
simulate_delays: self.simulate_delays,
fail_next_operation: Arc::clone(&self.fail_next_operation),
}
}
}
impl FakeTdClient {
pub fn new() -> Self {
Self {
chats: Arc::new(Mutex::new(vec![])),
messages: Arc::new(Mutex::new(HashMap::new())),
folders: Arc::new(Mutex::new(vec![FolderInfo { id: 0, name: "All".to_string() }])),
user_names: Arc::new(Mutex::new(HashMap::new())),
profiles: Arc::new(Mutex::new(HashMap::new())),
drafts: Arc::new(Mutex::new(HashMap::new())),
available_reactions: Arc::new(Mutex::new(vec![
"👍".to_string(), "❤️".to_string(), "😂".to_string(), "😮".to_string(),
"😢".to_string(), "🙏".to_string(), "👏".to_string(), "🔥".to_string(),
])),
network_state: Arc::new(Mutex::new(NetworkState::Ready)),
typing_chat_id: Arc::new(Mutex::new(None)),
current_chat_id: Arc::new(Mutex::new(None)),
current_pinned_message: Arc::new(Mutex::new(None)),
auth_state: Arc::new(Mutex::new(AuthState::Ready)),
sent_messages: Arc::new(Mutex::new(vec![])),
edited_messages: Arc::new(Mutex::new(vec![])),
deleted_messages: Arc::new(Mutex::new(vec![])),
forwarded_messages: Arc::new(Mutex::new(vec![])),
searched_queries: Arc::new(Mutex::new(vec![])),
viewed_messages: Arc::new(Mutex::new(vec![])),
chat_actions: Arc::new(Mutex::new(vec![])),
update_tx: Arc::new(Mutex::new(None)),
simulate_delays: false,
fail_next_operation: Arc::new(Mutex::new(false)),
}
}
/// Создать update channel для получения событий
pub fn with_update_channel(self) -> (Self, mpsc::UnboundedReceiver<TdUpdate>) {
let (tx, rx) = mpsc::unbounded_channel();
*self.update_tx.lock().unwrap() = Some(tx);
(self, rx)
}
/// Включить симуляцию задержек (как в реальном TDLib)
pub fn with_delays(mut self) -> Self {
self.simulate_delays = true;
self
}
// ==================== Builder Methods ====================
/// Добавить чат
pub fn with_chat(self, chat: ChatInfo) -> Self {
self.chats.lock().unwrap().push(chat);
self
}
/// Добавить несколько чатов
pub fn with_chats(self, chats: Vec<ChatInfo>) -> Self {
self.chats.lock().unwrap().extend(chats);
self
}
/// Добавить сообщение в чат
pub fn with_message(self, chat_id: i64, message: MessageInfo) -> Self {
self.messages
.lock()
.unwrap()
.entry(chat_id)
.or_insert_with(Vec::new)
.push(message);
self
}
/// Добавить несколько сообщений в чат
pub fn with_messages(self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
self.messages
.lock()
.unwrap()
.insert(chat_id, messages);
self
}
/// Добавить папку
pub fn with_folder(self, id: i32, name: &str) -> Self {
self.folders.lock().unwrap().push(FolderInfo { id, name: name.to_string() });
self
}
/// Добавить пользователя
pub fn with_user(self, id: i64, name: &str) -> Self {
self.user_names.lock().unwrap().insert(id, name.to_string());
self
}
/// Добавить профиль
pub fn with_profile(self, chat_id: i64, profile: ProfileInfo) -> Self {
self.profiles.lock().unwrap().insert(chat_id, profile);
self
}
/// Установить состояние сети
pub fn with_network_state(self, state: NetworkState) -> Self {
*self.network_state.lock().unwrap() = state;
self
}
/// Установить состояние авторизации
pub fn with_auth_state(self, state: AuthState) -> Self {
*self.auth_state.lock().unwrap() = state;
self
}
/// Установить доступные реакции
pub fn with_available_reactions(self, reactions: Vec<String>) -> Self {
*self.available_reactions.lock().unwrap() = reactions;
self
}
// ==================== Async TDLib Operations ====================
/// Загрузить список чатов
pub async fn load_chats(&self, limit: usize) -> Result<Vec<ChatInfo>, String> {
if self.should_fail() {
return Err("Failed to load chats".to_string());
}
if self.simulate_delays {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
let chats = self.chats.lock().unwrap().iter().take(limit).cloned().collect();
Ok(chats)
}
/// Открыть чат
pub async fn open_chat(&self, chat_id: ChatId) -> Result<(), String> {
if self.should_fail() {
return Err("Failed to open chat".to_string());
}
*self.current_chat_id.lock().unwrap() = Some(chat_id.as_i64());
Ok(())
}
/// Получить историю чата
pub async fn get_chat_history(&self, chat_id: ChatId, limit: i32) -> Result<Vec<MessageInfo>, String> {
if self.should_fail() {
return Err("Failed to load history".to_string());
}
if self.simulate_delays {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
let messages = self.messages
.lock()
.unwrap()
.get(&chat_id.as_i64())
.map(|msgs| msgs.iter().take(limit as usize).cloned().collect())
.unwrap_or_default();
Ok(messages)
}
/// Загрузить старые сообщения
pub async fn load_older_messages(&self, chat_id: ChatId, from_message_id: MessageId) -> Result<Vec<MessageInfo>, String> {
if self.should_fail() {
return Err("Failed to load older messages".to_string());
}
let messages = self.messages.lock().unwrap();
let chat_messages = messages.get(&chat_id.as_i64()).ok_or("Chat not found")?;
// Найти индекс сообщения и вернуть предыдущие
if let Some(idx) = chat_messages.iter().position(|m| m.id() == from_message_id) {
let older: Vec<_> = chat_messages.iter().take(idx).cloned().collect();
Ok(older)
} else {
Ok(vec![])
}
}
/// Отправить сообщение
pub async fn send_message(
&self,
chat_id: ChatId,
text: String,
reply_to: Option<MessageId>,
reply_info: Option<ReplyInfo>,
) -> Result<MessageInfo, String> {
if self.should_fail() {
return Err("Failed to send message".to_string());
}
if self.simulate_delays {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
}
let message_id = MessageId::new((self.sent_messages.lock().unwrap().len() as i64) + 1000);
self.sent_messages.lock().unwrap().push(SentMessage {
chat_id: chat_id.as_i64(),
text: text.clone(),
reply_to,
reply_info: reply_info.clone(),
});
let message = MessageInfo::new(
message_id,
"You".to_string(),
true, // is_outgoing
text.clone(),
vec![], // entities
chrono::Utc::now().timestamp() as i32,
0,
false, // is_read (станет true после Update)
true, // can_be_edited
true, // can_be_deleted_only_for_self
true, // can_be_deleted_for_all_users
reply_info,
None, // forward_from
vec![], // reactions
);
// Добавляем в историю
self.messages
.lock()
.unwrap()
.entry(chat_id.as_i64())
.or_insert_with(Vec::new)
.push(message.clone());
// Отправляем Update::NewMessage
self.send_update(TdUpdate::NewMessage {
chat_id,
message: message.clone(),
});
Ok(message)
}
/// Редактировать сообщение
pub async fn edit_message(
&self,
chat_id: ChatId,
message_id: MessageId,
new_text: String,
) -> Result<MessageInfo, String> {
if self.should_fail() {
return Err("Failed to edit message".to_string());
}
if self.simulate_delays {
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
}
self.edited_messages.lock().unwrap().push(EditedMessage {
chat_id: chat_id.as_i64(),
message_id,
new_text: new_text.clone(),
});
// Обновляем сообщение
let mut messages = self.messages.lock().unwrap();
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
msg.content.text = new_text.clone();
msg.metadata.edit_date = msg.metadata.date + 60;
let updated = msg.clone();
drop(messages); // Освобождаем lock перед отправкой update
// Отправляем Update
self.send_update(TdUpdate::MessageContent {
chat_id,
message_id,
new_text,
});
return Ok(updated);
}
}
Err("Message not found".to_string())
}
/// Удалить сообщения
pub async fn delete_messages(
&self,
chat_id: ChatId,
message_ids: Vec<MessageId>,
revoke: bool,
) -> Result<(), String> {
if self.should_fail() {
return Err("Failed to delete messages".to_string());
}
if self.simulate_delays {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
self.deleted_messages.lock().unwrap().push(DeletedMessages {
chat_id: chat_id.as_i64(),
message_ids: message_ids.clone(),
revoke,
});
// Удаляем из истории
let mut messages = self.messages.lock().unwrap();
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
chat_msgs.retain(|m| !message_ids.contains(&m.id()));
}
drop(messages);
// Отправляем Update
self.send_update(TdUpdate::DeleteMessages {
chat_id,
message_ids,
});
Ok(())
}
/// Переслать сообщения
pub async fn forward_messages(
&self,
to_chat_id: ChatId,
from_chat_id: ChatId,
message_ids: Vec<MessageId>,
) -> Result<(), String> {
if self.should_fail() {
return Err("Failed to forward messages".to_string());
}
if self.simulate_delays {
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
}
self.forwarded_messages.lock().unwrap().push(ForwardedMessages {
from_chat_id: from_chat_id.as_i64(),
to_chat_id: to_chat_id.as_i64(),
message_ids,
});
Ok(())
}
/// Поиск сообщений в чате
pub async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result<Vec<MessageInfo>, String> {
if self.should_fail() {
return Err("Failed to search messages".to_string());
}
let messages = self.messages.lock().unwrap();
let results: Vec<_> = messages
.get(&chat_id.as_i64())
.map(|msgs| {
msgs.iter()
.filter(|m| m.text().to_lowercase().contains(&query.to_lowercase()))
.cloned()
.collect()
})
.unwrap_or_default();
self.searched_queries.lock().unwrap().push(SearchQuery {
chat_id: chat_id.as_i64(),
query: query.to_string(),
results_count: results.len(),
});
Ok(results)
}
/// Установить черновик
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
if text.is_empty() {
self.drafts.lock().unwrap().remove(&chat_id.as_i64());
} else {
self.drafts.lock().unwrap().insert(chat_id.as_i64(), text.clone());
}
self.send_update(TdUpdate::ChatDraftMessage {
chat_id,
draft_text: if text.is_empty() { None } else { Some(text) },
});
Ok(())
}
/// Отправить действие в чате (typing, etc.)
pub async fn send_chat_action(&self, chat_id: ChatId, action: String) {
self.chat_actions.lock().unwrap().push((chat_id.as_i64(), action.clone()));
if action == "Typing" {
*self.typing_chat_id.lock().unwrap() = Some(chat_id.as_i64());
} else if action == "Cancel" {
*self.typing_chat_id.lock().unwrap() = None;
}
}
/// Получить доступные реакции для сообщения
pub async fn get_message_available_reactions(
&self,
_chat_id: ChatId,
_message_id: MessageId,
) -> Result<Vec<String>, String> {
if self.should_fail() {
return Err("Failed to get available reactions".to_string());
}
Ok(self.available_reactions.lock().unwrap().clone())
}
/// Установить/удалить реакцию
pub async fn toggle_reaction(
&self,
chat_id: ChatId,
message_id: MessageId,
emoji: String,
) -> Result<(), String> {
if self.should_fail() {
return Err("Failed to toggle reaction".to_string());
}
// Обновляем реакции на сообщении
let mut messages = self.messages.lock().unwrap();
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
let reactions = &mut msg.interactions.reactions;
// Toggle logic
if let Some(pos) = reactions.iter().position(|r| r.emoji == emoji && r.is_chosen) {
// Удаляем свою реакцию
reactions.remove(pos);
} else if let Some(reaction) = reactions.iter_mut().find(|r| r.emoji == emoji) {
// Добавляем себя к существующей реакции
reaction.is_chosen = true;
reaction.count += 1;
} else {
// Добавляем новую реакцию
reactions.push(ReactionInfo {
emoji: emoji.clone(),
count: 1,
is_chosen: true,
});
}
let updated_reactions = reactions.clone();
drop(messages);
// Отправляем Update
self.send_update(TdUpdate::MessageInteractionInfo {
chat_id,
message_id,
reactions: updated_reactions,
});
}
}
Ok(())
}
/// Получить информацию о профиле
pub async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
if self.should_fail() {
return Err("Failed to get profile info".to_string());
}
self.profiles
.lock()
.unwrap()
.get(&chat_id.as_i64())
.cloned()
.ok_or_else(|| "Profile not found".to_string())
}
/// Отметить сообщения как просмотренные
pub async fn view_messages(&self, chat_id: ChatId, message_ids: Vec<MessageId>) {
self.viewed_messages
.lock()
.unwrap()
.push((chat_id.as_i64(), message_ids.iter().map(|id| id.as_i64()).collect()));
}
/// Загрузить чаты папки
pub async fn load_folder_chats(&self, _folder_id: i32, _limit: usize) -> Result<(), String> {
if self.should_fail() {
return Err("Failed to load folder chats".to_string());
}
Ok(())
}
// ==================== Helper Methods ====================
/// Отправить update в канал (если он установлен)
fn send_update(&self, update: TdUpdate) {
if let Some(tx) = self.update_tx.lock().unwrap().as_ref() {
let _ = tx.send(update);
}
}
/// Проверить нужно ли симулировать ошибку
fn should_fail(&self) -> bool {
let mut fail = self.fail_next_operation.lock().unwrap();
if *fail {
*fail = false; // Сбрасываем после первого использования
true
} else {
false
}
}
/// Симулировать ошибку в следующей операции
pub fn fail_next(&self) {
*self.fail_next_operation.lock().unwrap() = true;
}
/// Симулировать входящее сообщение
pub fn simulate_incoming_message(&self, chat_id: ChatId, text: String, sender_name: &str) {
let message_id = MessageId::new(9000 + chrono::Utc::now().timestamp());
let message = MessageInfo::new(
message_id,
sender_name.to_string(),
false, // is_outgoing
text,
vec![],
chrono::Utc::now().timestamp() as i32,
0,
false,
false,
false,
true,
None,
None,
vec![],
);
// Добавляем в историю
self.messages
.lock()
.unwrap()
.entry(chat_id.as_i64())
.or_insert_with(Vec::new)
.push(message.clone());
// Отправляем Update
self.send_update(TdUpdate::NewMessage { chat_id, message });
}
/// Симулировать typing от собеседника
pub fn simulate_typing(&self, chat_id: ChatId, user_id: UserId) {
self.send_update(TdUpdate::ChatAction {
chat_id,
user_id,
action: "Typing".to_string(),
});
}
/// Симулировать изменение состояния сети
pub fn simulate_network_change(&self, state: NetworkState) {
*self.network_state.lock().unwrap() = state.clone();
self.send_update(TdUpdate::ConnectionState { state });
}
/// Симулировать прочтение сообщений
pub fn simulate_read_outbox(&self, chat_id: ChatId, last_read_message_id: MessageId) {
self.send_update(TdUpdate::ChatReadOutbox {
chat_id,
last_read_outbox_message_id: last_read_message_id,
});
}
// ==================== Getters for Test Assertions ====================
/// Получить все чаты
pub fn get_chats(&self) -> Vec<ChatInfo> {
self.chats.lock().unwrap().clone()
}
/// Получить все папки
pub fn get_folders(&self) -> Vec<FolderInfo> {
self.folders.lock().unwrap().clone()
}
/// Получить сообщения чата
pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> {
self.messages
.lock()
.unwrap()
.get(&chat_id)
.cloned()
.unwrap_or_default()
}
/// Получить отправленные сообщения
pub fn get_sent_messages(&self) -> Vec<SentMessage> {
self.sent_messages.lock().unwrap().clone()
}
/// Получить отредактированные сообщения
pub fn get_edited_messages(&self) -> Vec<EditedMessage> {
self.edited_messages.lock().unwrap().clone()
}
/// Получить удалённые сообщения
pub fn get_deleted_messages(&self) -> Vec<DeletedMessages> {
self.deleted_messages.lock().unwrap().clone()
}
/// Получить пересланные сообщения
pub fn get_forwarded_messages(&self) -> Vec<ForwardedMessages> {
self.forwarded_messages.lock().unwrap().clone()
}
/// Получить поисковые запросы
pub fn get_search_queries(&self) -> Vec<SearchQuery> {
self.searched_queries.lock().unwrap().clone()
}
/// Получить просмотренные сообщения
pub fn get_viewed_messages(&self) -> Vec<(i64, Vec<i64>)> {
self.viewed_messages.lock().unwrap().clone()
}
/// Получить действия в чатах
pub fn get_chat_actions(&self) -> Vec<(i64, String)> {
self.chat_actions.lock().unwrap().clone()
}
/// Получить текущее состояние сети
pub fn get_network_state(&self) -> NetworkState {
self.network_state.lock().unwrap().clone()
}
/// Получить ID текущего открытого чата
pub fn get_current_chat_id(&self) -> Option<i64> {
*self.current_chat_id.lock().unwrap()
}
/// Установить update channel для получения событий
pub fn set_update_channel(&self, tx: mpsc::UnboundedSender<TdUpdate>) {
*self.update_tx.lock().unwrap() = Some(tx);
}
/// Очистить всю историю действий
pub fn clear_all_history(&self) {
self.sent_messages.lock().unwrap().clear();
self.edited_messages.lock().unwrap().clear();
self.deleted_messages.lock().unwrap().clear();
self.forwarded_messages.lock().unwrap().clear();
self.searched_queries.lock().unwrap().clear();
self.viewed_messages.lock().unwrap().clear();
self.chat_actions.lock().unwrap().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::test_data::create_test_chat;
use tele_tui::types::ChatId;
#[test]
fn test_fake_client_creation() {
let client = FakeTdClient::new();
assert_eq!(client.get_chats().len(), 0);
assert_eq!(client.folders.lock().unwrap().len(), 1); // Default "All" folder
}
#[test]
fn test_fake_client_with_chat() {
let chat = create_test_chat("Mom", 123);
let client = FakeTdClient::new().with_chat(chat);
let chats = client.get_chats();
assert_eq!(chats.len(), 1);
assert_eq!(chats[0].title, "Mom");
}
#[tokio::test]
async fn test_send_message() {
let client = FakeTdClient::new();
let chat_id = ChatId::new(123);
let result = client.send_message(chat_id, "Hello".to_string(), None, None).await;
assert!(result.is_ok());
let sent = client.get_sent_messages();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0].text, "Hello");
assert_eq!(client.get_messages(123).len(), 1);
}
#[tokio::test]
async fn test_edit_message() {
let client = FakeTdClient::new();
let chat_id = ChatId::new(123);
let msg = client.send_message(chat_id, "Hello".to_string(), None, None).await.unwrap();
let msg_id = msg.id();
let _ = client.edit_message(chat_id, msg_id, "Hello World".to_string()).await;
let edited = client.get_edited_messages();
assert_eq!(edited.len(), 1);
assert_eq!(client.get_messages(123)[0].text(), "Hello World");
assert!(client.get_messages(123)[0].metadata.edit_date > 0);
}
#[tokio::test]
async fn test_delete_message() {
let client = FakeTdClient::new();
let chat_id = ChatId::new(123);
let msg = client.send_message(chat_id, "Hello".to_string(), None, None).await.unwrap();
let msg_id = msg.id();
let _ = client.delete_messages(chat_id, vec![msg_id], false).await;
let deleted = client.get_deleted_messages();
assert_eq!(deleted.len(), 1);
assert_eq!(client.get_messages(123).len(), 0);
}
#[tokio::test]
async fn test_update_channel() {
let (client, mut rx) = FakeTdClient::new().with_update_channel();
let chat_id = ChatId::new(123);
// Отправляем сообщение
let _ = client.send_message(chat_id, "Test".to_string(), None, None).await;
// Проверяем что получили Update
if let Some(update) = rx.recv().await {
match update {
TdUpdate::NewMessage { chat_id: updated_chat, .. } => {
assert_eq!(updated_chat, chat_id);
}
_ => panic!("Expected NewMessage update"),
}
} else {
panic!("No update received");
}
}
#[tokio::test]
async fn test_simulate_incoming_message() {
let (client, mut rx) = FakeTdClient::new().with_update_channel();
let chat_id = ChatId::new(123);
client.simulate_incoming_message(chat_id, "Hello from Bob".to_string(), "Bob");
// Проверяем Update
if let Some(TdUpdate::NewMessage { message, .. }) = rx.recv().await {
assert_eq!(message.text(), "Hello from Bob");
assert_eq!(message.sender_name(), "Bob");
assert!(!message.is_outgoing());
}
// Проверяем что сообщение добавилось
assert_eq!(client.get_messages(123).len(), 1);
}
#[tokio::test]
async fn test_fail_next_operation() {
let client = FakeTdClient::new();
let chat_id = ChatId::new(123);
// Устанавливаем флаг ошибки
client.fail_next();
// Следующая операция должна упасть
let result = client.send_message(chat_id, "Test".to_string(), None, None).await;
assert!(result.is_err());
// Но следующая должна пройти
let result2 = client.send_message(chat_id, "Test2".to_string(), None, None).await;
assert!(result2.is_ok());
}
}