Files
telegram-tui/tests/helpers/app_builder.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

345 lines
10 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.
// Test App builder
use ratatui::widgets::ListState;
use std::collections::HashMap;
use super::FakeTdClient;
use tele_tui::app::{App, AppScreen, ChatState};
use tele_tui::config::Config;
use tele_tui::tdlib::AuthState;
use tele_tui::tdlib::{ChatInfo, MessageInfo};
use tele_tui::types::{ChatId, MessageId};
/// Builder для создания тестового App с FakeTdClient\n///\n/// Использует trait-based DI для подмены TdClient на FakeTdClient в тестах.
pub struct TestAppBuilder {
config: Config,
screen: AppScreen,
chats: Vec<ChatInfo>,
selected_chat_id: Option<i64>,
message_input: String,
is_searching: bool,
search_query: String,
chat_state: Option<ChatState>,
messages: HashMap<i64, Vec<MessageInfo>>,
status_message: Option<String>,
auth_state: Option<AuthState>,
phone_input: Option<String>,
code_input: Option<String>,
password_input: Option<String>,
}
impl Default for TestAppBuilder {
fn default() -> Self {
Self::new()
}
}
impl TestAppBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
screen: AppScreen::Main,
chats: vec![],
selected_chat_id: None,
message_input: String::new(),
is_searching: false,
search_query: String::new(),
chat_state: None,
messages: HashMap::new(),
status_message: None,
auth_state: None,
phone_input: None,
code_input: None,
password_input: None,
}
}
/// Установить экран
pub fn screen(mut self, screen: AppScreen) -> Self {
self.screen = screen;
self
}
/// Установить конфиг
pub fn config(mut self, config: Config) -> Self {
self.config = config;
self
}
/// Добавить чат
pub fn with_chat(mut self, chat: ChatInfo) -> Self {
self.chats.push(chat);
self
}
/// Добавить несколько чатов
pub fn with_chats(mut self, chats: Vec<ChatInfo>) -> Self {
self.chats.extend(chats);
self
}
/// Выбрать чат
pub fn selected_chat(mut self, chat_id: i64) -> Self {
self.selected_chat_id = Some(chat_id);
self
}
/// Установить текст в инпуте
pub fn message_input(mut self, text: &str) -> Self {
self.message_input = text.to_string();
self
}
/// Режим поиска
pub fn searching(mut self, query: &str) -> Self {
self.is_searching = true;
self.search_query = query.to_string();
self
}
/// Режим редактирования сообщения
pub fn editing_message(mut self, message_id: i64, selected_index: usize) -> Self {
self.chat_state = Some(ChatState::Editing {
message_id: MessageId::new(message_id),
selected_index,
});
self
}
/// Режим ответа на сообщение
pub fn replying_to(mut self, message_id: i64) -> Self {
self.chat_state = Some(ChatState::Reply { message_id: MessageId::new(message_id) });
self
}
/// Режим выбора реакции
pub fn reaction_picker(mut self, message_id: i64, available_reactions: Vec<String>) -> Self {
self.chat_state = Some(ChatState::ReactionPicker {
message_id: MessageId::new(message_id),
available_reactions,
selected_index: 0,
});
self
}
/// Режим профиля
pub fn profile_mode(mut self, info: tele_tui::tdlib::ProfileInfo) -> Self {
self.chat_state = Some(ChatState::Profile {
info,
selected_action: 0,
leave_group_confirmation_step: 0,
});
self
}
/// Подтверждение удаления
pub fn delete_confirmation(mut self, message_id: i64) -> Self {
self.chat_state = Some(ChatState::DeleteConfirmation { message_id: MessageId::new(message_id) });
self
}
/// Добавить сообщение для чата
pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.push(message);
self
}
/// Добавить несколько сообщений для чата
pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.extend(messages);
self
}
/// Установить выбранное сообщение (режим selection)
pub fn selecting_message(mut self, selected_index: usize) -> Self {
self.chat_state = Some(ChatState::MessageSelection { selected_index });
self
}
/// Режим поиска по сообщениям в чате
pub fn message_search(mut self, query: &str) -> Self {
self.chat_state = Some(ChatState::SearchInChat {
query: query.to_string(),
results: Vec::new(),
selected_index: 0,
});
self
}
/// Режим пересылки сообщения
pub fn forward_mode(mut self, message_id: i64) -> Self {
self.chat_state = Some(ChatState::Forward {
message_id: MessageId::new(message_id),
selecting_chat: true,
});
self
}
/// Установить статус сообщение (для loading screen)
pub fn status_message(mut self, message: &str) -> Self {
self.status_message = Some(message.to_string());
self
}
/// Установить auth state
pub fn auth_state(mut self, state: AuthState) -> Self {
self.auth_state = Some(state);
self
}
/// Установить phone input
pub fn phone_input(mut self, phone: &str) -> Self {
self.phone_input = Some(phone.to_string());
self
}
/// Установить code input
pub fn code_input(mut self, code: &str) -> Self {
self.code_input = Some(code.to_string());
self
}
/// Установить password input
pub fn password_input(mut self, password: &str) -> Self {
self.password_input = Some(password.to_string());
self
}
/// Построить App с FakeTdClient
///
/// Создаёт App с FakeTdClient, подходит для любых тестов включая
/// интеграционные тесты логики.
pub fn build(self) -> App<FakeTdClient> {
// Создаём FakeTdClient с чатами и сообщениями
let mut fake_client = FakeTdClient::new();
// Добавляем чаты
for chat in &self.chats {
fake_client = fake_client.with_chat(chat.clone());
}
// Добавляем сообщения
for (chat_id, messages) in self.messages {
fake_client = fake_client.with_messages(chat_id, messages);
}
// Устанавливаем текущий чат если нужно
if let Some(chat_id) = self.selected_chat_id {
*fake_client.current_chat_id.lock().unwrap() = Some(chat_id);
}
// Устанавливаем auth state если нужно
if let Some(auth_state) = self.auth_state {
fake_client = fake_client.with_auth_state(auth_state);
}
// Создаём App с FakeTdClient
let mut app = App::with_client(self.config, fake_client);
app.screen = self.screen;
app.chats = self.chats;
app.selected_chat_id = self.selected_chat_id.map(ChatId::new);
app.message_input = self.message_input;
app.is_searching = self.is_searching;
app.search_query = self.search_query;
// Применяем chat_state если он установлен
if let Some(chat_state) = self.chat_state {
app.chat_state = chat_state;
}
// Применяем status_message
if let Some(status) = self.status_message {
app.status_message = Some(status);
}
// Применяем auth inputs
if let Some(phone) = self.phone_input {
app.phone_input = phone;
}
if let Some(code) = self.code_input {
app.code_input = code;
}
if let Some(password) = self.password_input {
app.password_input = password;
}
// Выбираем первый чат если есть
if !app.chats.is_empty() {
let mut list_state = ListState::default();
list_state.select(Some(0));
app.chat_list_state = list_state;
}
app
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::test_data::create_test_chat;
#[test]
fn test_builder_defaults() {
let app = TestAppBuilder::new().build();
assert_eq!(app.screen, AppScreen::Main);
assert_eq!(app.chats.len(), 0);
assert_eq!(app.selected_chat_id, None);
assert_eq!(app.message_input, "");
}
#[test]
fn test_builder_with_chats() {
let chat1 = create_test_chat("Mom", 123);
let chat2 = create_test_chat("Boss", 456);
let app = TestAppBuilder::new()
.with_chat(chat1)
.with_chat(chat2)
.build();
assert_eq!(app.chats.len(), 2);
assert_eq!(app.chats[0].title, "Mom");
assert_eq!(app.chats[1].title, "Boss");
}
#[test]
fn test_builder_with_selected_chat() {
let chat = create_test_chat("Mom", 123);
let app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.build();
assert_eq!(app.selected_chat_id, Some(ChatId::new(123)));
}
#[test]
fn test_builder_editing_mode() {
let app = TestAppBuilder::new()
.editing_message(999, 0)
.message_input("Edited text")
.build();
assert!(app.is_editing());
assert_eq!(app.chat_state.selected_message_id(), Some(MessageId::new(999)));
assert_eq!(app.message_input, "Edited text");
}
#[test]
fn test_builder_search_mode() {
let app = TestAppBuilder::new().searching("test query").build();
assert!(app.is_searching);
assert_eq!(app.search_query, "test query");
}
}