From 1232b2fc6540595fe683115bb3499ccddf1d7270 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 28 Dec 2025 11:51:00 -0300 Subject: [PATCH] Test framework improvements and fixture updates --- src/bot/conversation.rs | 53 ++++++++++++------------- src/desktop/mod.rs | 1 + src/fixtures/data/mod.rs | 16 +++++--- src/fixtures/mod.rs | 4 +- src/harness.rs | 29 ++++++++------ src/lib.rs | 3 +- src/main.rs | 23 +++++------ src/mocks/llm.rs | 45 +++++++++++---------- src/mocks/teams.rs | 7 ++-- src/mocks/whatsapp.rs | 10 ++--- src/mocks/zitadel.rs | 10 ++--- src/services/browser_service.rs | 2 +- src/services/minio.rs | 28 ++++++------- src/services/redis.rs | 28 +++++-------- src/web/mod.rs | 2 +- tests/e2e/auth_flow.rs | 33 +++++++-------- tests/e2e/chat.rs | 64 +++++++++++++----------------- tests/e2e/dashboard.rs | 13 +++--- tests/e2e/mod.rs | 2 +- tests/integration/api.rs | 23 +---------- tests/integration/basic_runtime.rs | 29 +++++--------- tests/integration/database.rs | 34 ++++++++-------- tests/integration/mod.rs | 10 ----- tests/unit/mod.rs | 5 +-- 24 files changed, 208 insertions(+), 266 deletions(-) diff --git a/src/bot/conversation.rs b/src/bot/conversation.rs index 41cb361..cd3e4dd 100644 --- a/src/bot/conversation.rs +++ b/src/bot/conversation.rs @@ -131,7 +131,7 @@ impl ConversationTest { ConversationBuilder::new(bot_name).build() } - pub async fn with_context(ctx: &TestContext, bot_name: &str) -> Result { + pub fn with_context(ctx: &TestContext, bot_name: &str) -> Result { let mut conv = ConversationBuilder::new(bot_name).build(); conv.llm_url = Some(ctx.llm_url()); Ok(conv) @@ -306,7 +306,7 @@ impl ConversationTest { metadata } - pub async fn assert_response_contains(&mut self, text: &str) -> &mut Self { + pub fn assert_response_contains(&mut self, text: &str) -> &mut Self { let result = if let Some(ref response) = self.last_response { if response.content.contains(text) { AssertionResult::pass(&format!("Response contains '{text}'")) @@ -325,7 +325,7 @@ impl ConversationTest { self } - pub async fn assert_response_equals(&mut self, text: &str) -> &mut Self { + pub fn assert_response_equals(&mut self, text: &str) -> &mut Self { let result = if let Some(ref response) = self.last_response { if response.content == text { AssertionResult::pass(&format!("Response equals '{text}'")) @@ -344,7 +344,7 @@ impl ConversationTest { self } - pub async fn assert_response_matches(&mut self, pattern: &str) -> &mut Self { + pub fn assert_response_matches(&mut self, pattern: &str) -> &mut Self { let result = if let Some(ref response) = self.last_response { match regex::Regex::new(pattern) { Ok(re) => { @@ -372,7 +372,7 @@ impl ConversationTest { self } - pub async fn assert_response_not_contains(&mut self, text: &str) -> &mut Self { + pub fn assert_response_not_contains(&mut self, text: &str) -> &mut Self { let result = if let Some(ref response) = self.last_response { if response.content.contains(text) { AssertionResult::fail( @@ -391,16 +391,13 @@ impl ConversationTest { self } - pub async fn assert_transferred_to_human(&mut self) -> &mut Self { + pub fn assert_transferred_to_human(&mut self) -> &mut Self { let is_transferred = self.state == ConversationState::Transferred - || self - .last_response - .as_ref() - .is_some_and(|r| { - r.content.to_lowercase().contains("transfer") - || r.content.to_lowercase().contains("human") - || r.content.to_lowercase().contains("agent") - }); + || self.last_response.as_ref().is_some_and(|r| { + r.content.to_lowercase().contains("transfer") + || r.content.to_lowercase().contains("human") + || r.content.to_lowercase().contains("agent") + }); let result = if is_transferred { self.state = ConversationState::Transferred; @@ -417,7 +414,7 @@ impl ConversationTest { self } - pub async fn assert_queue_position(&mut self, expected: usize) -> &mut Self { + pub fn assert_queue_position(&mut self, expected: usize) -> &mut Self { let actual = self .context .get("queue_position") @@ -438,7 +435,7 @@ impl ConversationTest { self } - pub async fn assert_response_within(&mut self, max_duration: Duration) -> &mut Self { + pub fn assert_response_within(&mut self, max_duration: Duration) -> &mut Self { let result = if let Some(latency) = self.last_latency { if latency <= max_duration { AssertionResult::pass(&format!("Response within {max_duration:?}")) @@ -461,7 +458,7 @@ impl ConversationTest { self } - pub async fn assert_response_count(&mut self, expected: usize) -> &mut Self { + pub fn assert_response_count(&mut self, expected: usize) -> &mut Self { let actual = self.responses.len(); let result = if actual == expected { @@ -478,7 +475,7 @@ impl ConversationTest { self } - pub async fn assert_response_type(&mut self, expected: ResponseContentType) -> &mut Self { + pub fn assert_response_type(&mut self, expected: ResponseContentType) -> &mut Self { let result = if let Some(ref response) = self.last_response { if response.content_type == expected { AssertionResult::pass(&format!("Response type is {expected:?}")) @@ -510,7 +507,7 @@ impl ConversationTest { self.context.get(key) } - pub async fn end(&mut self) -> &mut Self { + pub fn end(&mut self) -> &mut Self { self.state = ConversationState::Ended; self.record.ended_at = Some(Utc::now()); self @@ -583,7 +580,7 @@ mod tests { async fn test_assert_response_contains() { let mut conv = ConversationTest::new("test-bot"); conv.user_says("test").await; - conv.assert_response_contains("Response").await; + conv.assert_response_contains("Response"); assert!(conv.all_passed()); } @@ -592,7 +589,7 @@ mod tests { async fn test_assert_response_not_contains() { let mut conv = ConversationTest::new("test-bot"); conv.user_says("test").await; - conv.assert_response_not_contains("nonexistent").await; + conv.assert_response_not_contains("nonexistent"); assert!(conv.all_passed()); } @@ -632,7 +629,7 @@ mod tests { async fn test_end_conversation() { let mut conv = ConversationTest::new("test-bot"); conv.user_says("bye").await; - conv.end().await; + conv.end(); assert_eq!(conv.state(), ConversationState::Ended); assert!(conv.record().ended_at.is_some()); @@ -642,7 +639,7 @@ mod tests { async fn test_failed_assertions() { let mut conv = ConversationTest::new("test-bot"); conv.user_says("test").await; - conv.assert_response_equals("this will not match").await; + conv.assert_response_equals("this will not match"); assert!(!conv.all_passed()); assert_eq!(conv.failed_assertions().len(), 1); @@ -672,13 +669,13 @@ mod tests { let mut conv = ConversationTest::new("support-bot"); conv.user_says("Hi").await; - conv.assert_response_contains("Response").await; + conv.assert_response_contains("Response"); conv.user_says("I need help").await; - conv.assert_response_contains("Response").await; + conv.assert_response_contains("Response"); conv.user_says("Thanks, bye").await; - conv.end().await; + conv.end(); assert_eq!(conv.sent_messages().len(), 3); assert_eq!(conv.responses().len(), 3); @@ -689,7 +686,7 @@ mod tests { async fn test_response_time_assertion() { let mut conv = ConversationTest::new("test-bot"); conv.user_says("quick test").await; - conv.assert_response_within(Duration::from_secs(5)).await; + conv.assert_response_within(Duration::from_secs(5)); assert!(conv.all_passed()); } @@ -699,7 +696,7 @@ mod tests { let mut conv = ConversationTest::new("test-bot"); conv.user_says("one").await; conv.user_says("two").await; - conv.assert_response_count(2).await; + conv.assert_response_count(2); assert!(conv.all_passed()); } diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs index 82e4d22..9ebcf50 100644 --- a/src/desktop/mod.rs +++ b/src/desktop/mod.rs @@ -254,6 +254,7 @@ pub struct Screenshot { impl Screenshot { pub fn save(&self, path: impl Into) -> Result<()> { + let _ = (&self.data, self.width, self.height); let path = path.into(); anyhow::bail!("Screenshot save not yet implemented: {}", path.display()) } diff --git a/src/fixtures/data/mod.rs b/src/fixtures/data/mod.rs index 7b42a26..ccc31ae 100644 --- a/src/fixtures/data/mod.rs +++ b/src/fixtures/data/mod.rs @@ -1,4 +1,3 @@ - use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; @@ -334,6 +333,7 @@ pub fn sample_faqs() -> Vec { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(clippy::upper_case_acronyms)] pub struct FAQ { pub id: u32, pub question: String, @@ -425,10 +425,12 @@ mod tests { fn test_whatsapp_text_message() { let payload = whatsapp_text_message("15551234567", "Hello"); assert_eq!(payload["object"], "whatsapp_business_account"); - assert!(payload["entry"][0]["changes"][0]["value"]["messages"][0]["text"]["body"] - .as_str() - .unwrap() - .contains("Hello")); + assert!( + payload["entry"][0]["changes"][0]["value"]["messages"][0]["text"]["body"] + .as_str() + .unwrap() + .contains("Hello") + ); } #[test] @@ -453,7 +455,9 @@ mod tests { fn test_sample_kb_entries() { let entries = sample_kb_entries(); assert!(!entries.is_empty()); - assert!(entries.iter().any(|e| e.category == Some("products".to_string()))); + assert!(entries + .iter() + .any(|e| e.category == Some("products".to_string()))); } #[test] diff --git a/src/fixtures/mod.rs b/src/fixtures/mod.rs index f4d6b0e..76e86a2 100644 --- a/src/fixtures/mod.rs +++ b/src/fixtures/mod.rs @@ -1,4 +1,3 @@ - pub mod data; pub mod scripts; @@ -7,7 +6,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub id: Uuid, @@ -79,6 +77,7 @@ impl Default for Customer { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] +#[allow(clippy::upper_case_acronyms)] pub enum Channel { WhatsApp, Teams, @@ -278,7 +277,6 @@ impl Default for QueueStatus { } } - #[must_use] pub fn admin_user() -> User { User { diff --git a/src/harness.rs b/src/harness.rs index 9c5464f..4834681 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -493,14 +493,18 @@ impl BotServerInstance { if !stack_path.exists() { anyhow::bail!( - "Main botserver-stack not found at {stack_path:?}.\n\ - Run botserver once to initialize: cd ../botserver && cargo run" + "Main botserver-stack not found at {}.\n\ + Run botserver once to initialize: cd ../botserver && cargo run", + stack_path.display() ); } - log::info!("Starting botserver with MAIN stack at {stack_path:?}"); + log::info!( + "Starting botserver with MAIN stack at {}", + stack_path.display() + ); println!("šŸš€ Starting BotServer with main stack..."); - println!(" Stack: {stack_path:?}"); + println!(" Stack: {}", stack_path.display()); let process = std::process::Command::new(&botserver_bin_path) .current_dir(&botserver_dir) @@ -606,7 +610,7 @@ impl BotUIInstance { log::info!("Starting botui from: {botui_bin} on port {port}"); log::info!(" BOTUI_PORT={port}"); log::info!(" BOTSERVER_URL={botserver_url}"); - log::info!(" Working directory: {botui_dir:?}"); + log::info!(" Working directory: {}", botui_dir.display()); let process = std::process::Command::new(&botui_bin_path) .current_dir(&botui_dir) @@ -672,7 +676,7 @@ impl BotServerInstance { let stack_path = ctx.data_dir.join("botserver-stack"); std::fs::create_dir_all(&stack_path)?; let stack_path = stack_path.canonicalize().unwrap_or(stack_path); - log::info!("Created clean test stack at: {stack_path:?}"); + log::info!("Created clean test stack at: {}", stack_path.display()); let botserver_bin = std::env::var("BOTSERVER_BIN") .unwrap_or_else(|_| "../botserver/target/debug/botserver".to_string()); @@ -701,12 +705,12 @@ impl BotServerInstance { .unwrap_or_else(|_| PathBuf::from("../botserver")) }); - log::info!("Botserver working directory: {botserver_dir:?}"); - log::info!("Stack path (absolute): {stack_path:?}"); + log::info!("Botserver working directory: {}", botserver_dir.display()); + log::info!("Stack path (absolute): {}", stack_path.display()); let installers_path = botserver_dir.join("botserver-installers"); let installers_path = installers_path.canonicalize().unwrap_or(installers_path); - log::info!("Using installers from: {installers_path:?}"); + log::info!("Using installers from: {}", installers_path.display()); let process = std::process::Command::new(&botserver_bin_path) .current_dir(&botserver_dir) @@ -764,7 +768,7 @@ impl BotServerInstance { self.process.is_some() } - fn setup_test_stack_config(stack_path: &PathBuf, ctx: &TestContext) -> Result<()> { + fn setup_test_stack_config(stack_path: &std::path::Path, ctx: &TestContext) -> Result<()> { let directory_conf = stack_path.join("conf/directory"); std::fs::create_dir_all(&directory_conf)?; @@ -800,7 +804,7 @@ ExternalPort: {} Ok(()) } - fn generate_test_certificates(certs_dir: &PathBuf) -> Result<()> { + fn generate_test_certificates(certs_dir: &std::path::Path) -> Result<()> { use std::process::Command; let api_dir = certs_dir.join("api"); @@ -922,7 +926,8 @@ impl TestHarness { }; log::info!( - "Test {test_id} allocated ports: {ports:?}, data_dir: {data_dir:?}, use_existing_stack: {use_existing_stack}" + "Test {test_id} allocated ports: {ports:?}, data_dir: {}, use_existing_stack: {use_existing_stack}", + data_dir.display() ); let data_dir_str = data_dir.to_str().unwrap().to_string(); diff --git a/src/lib.rs b/src/lib.rs index 14f8e64..8f8e643 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ pub mod prelude { mod tests { #[test] fn test_library_loads() { - assert!(true); + let version = env!("CARGO_PKG_VERSION"); + assert!(!version.is_empty()); } } diff --git a/src/main.rs b/src/main.rs index 20c6025..dd40b26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -308,7 +308,7 @@ async fn download_file(url: &str, dest: &PathBuf) -> Result<()> { Ok(()) } -async fn extract_zip(zip_path: &PathBuf, dest_dir: &PathBuf) -> Result<()> { +fn extract_zip(zip_path: &PathBuf, dest_dir: &PathBuf) -> Result<()> { info!("Extracting: {:?} to {:?}", zip_path, dest_dir); let file = std::fs::File::open(zip_path)?; @@ -390,7 +390,7 @@ async fn setup_chromedriver(browser_path: &str) -> Result { let zip_path = cache_dir.join("chromedriver.zip"); download_file(&chromedriver_url, &zip_path).await?; - extract_zip(&zip_path, &cache_dir).await?; + extract_zip(&zip_path, &cache_dir)?; let extracted_driver = cache_dir.join("chromedriver-linux64").join("chromedriver"); let final_path = get_chromedriver_path(&major_version); @@ -439,7 +439,7 @@ async fn setup_chrome_for_testing() -> Result { let zip_path = cache_dir.join("chrome.zip"); download_file(&chrome_url, &zip_path).await?; - extract_zip(&zip_path, &cache_dir).await?; + extract_zip(&zip_path, &cache_dir)?; std::fs::remove_file(&zip_path).ok(); @@ -494,12 +494,11 @@ async fn start_chromedriver(chromedriver_path: &PathBuf, port: u16) -> Result bool { let url = format!("http://localhost:{port}/status"); - let client = match reqwest::Client::builder() + let Ok(client) = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(2)) .build() - { - Ok(c) => c, - Err(_) => return false, + else { + return false; }; client.get(&url).send().await.is_ok() @@ -641,7 +640,7 @@ fn run_cargo_test( Ok((passed, failed, skipped)) } -async fn run_unit_tests(config: &RunnerConfig) -> Result { +fn run_unit_tests(config: &RunnerConfig) -> Result { info!("Running unit tests..."); let mut results = TestResults::new("unit"); @@ -998,8 +997,8 @@ async fn main() -> ExitCode { match setup_test_dependencies().await { Ok((chromedriver, chrome)) => { println!("\nāœ… Dependencies installed successfully!"); - println!(" ChromeDriver: {chromedriver:?}"); - println!(" Browser: {chrome:?}"); + println!(" ChromeDriver: {}", chromedriver.display()); + println!(" Browser: {}", chrome.display()); return ExitCode::SUCCESS; } Err(e) => { @@ -1029,11 +1028,11 @@ async fn main() -> ExitCode { let mut all_results = Vec::new(); let result = match config.suite { - TestSuite::Unit => run_unit_tests(&config).await, + TestSuite::Unit => run_unit_tests(&config), TestSuite::Integration => run_integration_tests(&config).await, TestSuite::E2E => run_e2e_tests(&config).await, TestSuite::All => { - let unit = run_unit_tests(&config).await; + let unit = run_unit_tests(&config); let integration = run_integration_tests(&config).await; let e2e = run_e2e_tests(&config).await; diff --git a/src/mocks/llm.rs b/src/mocks/llm.rs index 2b8b28c..3cec005 100644 --- a/src/mocks/llm.rs +++ b/src/mocks/llm.rs @@ -1,6 +1,7 @@ use super::{new_expectation_store, Expectation, ExpectationStore}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; +use std::fmt::Write; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -89,6 +90,7 @@ struct ChatChoice { } #[derive(Serialize)] +#[allow(clippy::struct_field_names)] struct Usage { prompt_tokens: u32, completion_tokens: u32, @@ -219,11 +221,13 @@ impl MockLLM { .unwrap() .push(expectation.clone()); - let mut store = self.expectations.lock().unwrap(); - store.insert( - format!("completion:{prompt_contains}"), - Expectation::new(&format!("completion containing '{prompt_contains}'")), - ); + { + let mut store = self.expectations.lock().unwrap(); + store.insert( + format!("completion:{prompt_contains}"), + Expectation::new(&format!("completion containing '{prompt_contains}'")), + ); + } let response_text = response.to_string(); let model = self.default_model.clone(); @@ -253,7 +257,8 @@ impl MockLLM { let mut template = ResponseTemplate::new(200).set_body_json(&response_body); - if let Some(delay) = *latency.lock().unwrap() { + let latency_value = *latency.lock().unwrap(); + if let Some(delay) = latency_value { template = template.set_delay(delay); } @@ -303,10 +308,11 @@ impl MockLLM { finish_reason: None, }], }; - sse_body.push_str(&format!( - "data: {}\n\n", + let _ = writeln!( + sse_body, + "data: {}\n", serde_json::to_string(&first_chunk).unwrap() - )); + ); for chunk_text in &chunks { let chunk = StreamChunk { @@ -323,10 +329,11 @@ impl MockLLM { finish_reason: None, }], }; - sse_body.push_str(&format!( - "data: {}\n\n", + let _ = writeln!( + sse_body, + "data: {}\n", serde_json::to_string(&chunk).unwrap() - )); + ); } let final_chunk = StreamChunk { @@ -343,10 +350,11 @@ impl MockLLM { finish_reason: Some("stop".to_string()), }], }; - sse_body.push_str(&format!( - "data: {}\n\n", + let _ = writeln!( + sse_body, + "data: {}\n", serde_json::to_string(&final_chunk).unwrap() - )); + ); sse_body.push_str("data: [DONE]\n\n"); let template = ResponseTemplate::new(200) @@ -596,10 +604,7 @@ impl MockLLM { } pub async fn call_count(&self) -> usize { - self.server - .received_requests() - .await - .map_or(0, |r| r.len()) + self.server.received_requests().await.map_or(0, |r| r.len()) } pub async fn assert_called_times(&self, expected: usize) { @@ -649,7 +654,7 @@ mod tests { let response = ChatCompletionResponse { id: "test-id".to_string(), object: "chat.completion".to_string(), - created: 1234567890, + created: 1_234_567_890, model: "gpt-4".to_string(), choices: vec![ChatChoice { index: 0, diff --git a/src/mocks/teams.rs b/src/mocks/teams.rs index 681b525..8613a3c 100644 --- a/src/mocks/teams.rs +++ b/src/mocks/teams.rs @@ -1,4 +1,3 @@ - use super::{new_expectation_store, ExpectationStore}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -22,6 +21,7 @@ pub struct MockTeams { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(clippy::struct_field_names)] pub struct Activity { #[serde(rename = "type")] pub activity_type: String, @@ -124,6 +124,7 @@ pub struct Attachment { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(clippy::struct_field_names)] pub struct Entity { #[serde(rename = "type")] pub entity_type: String, @@ -318,9 +319,7 @@ impl MockTeams { sent_activities.lock().unwrap().push(activity.clone()); - let response = ResourceResponse { - id: activity.id, - }; + let response = ResourceResponse { id: activity.id }; ResponseTemplate::new(200).set_body_json(&response) }) diff --git a/src/mocks/whatsapp.rs b/src/mocks/whatsapp.rs index 42ee509..488a442 100644 --- a/src/mocks/whatsapp.rs +++ b/src/mocks/whatsapp.rs @@ -1,4 +1,3 @@ - use super::{new_expectation_store, ExpectationStore}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -213,7 +212,8 @@ pub struct ConversationOrigin { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Pricing { pub billable: bool, - pub pricing_model: String, + #[serde(alias = "pricing_model")] + pub model: String, pub category: String, } @@ -354,7 +354,6 @@ impl MockWhatsApp { async fn setup_default_routes(&self) { let sent_messages = self.sent_messages.clone(); - let _phone_id = self.phone_number_id.clone(); Mock::given(method("POST")) .and(path_regex(r"/v\d+\.\d+/\d+/messages")) @@ -413,7 +412,6 @@ impl MockWhatsApp { id: message_id.clone(), to: to.to_string(), message_type: match msg_type { - "text" => MessageType::Text, "template" => MessageType::Template, "image" => MessageType::Image, "document" => MessageType::Document, @@ -483,6 +481,7 @@ impl MockWhatsApp { #[must_use] pub fn expect_send_message(&self, to: &str) -> MessageExpectation { + let _ = self; MessageExpectation { to: to.to_string(), message_type: None, @@ -492,6 +491,7 @@ impl MockWhatsApp { #[must_use] pub fn expect_send_template(&self, name: &str) -> TemplateExpectation { + let _ = self; TemplateExpectation { name: name.to_string(), to: None, @@ -707,7 +707,7 @@ impl MockWhatsApp { }), pricing: Some(Pricing { billable: true, - pricing_model: "CBP".to_string(), + model: "CBP".to_string(), category: "business_initiated".to_string(), }), }]), diff --git a/src/mocks/zitadel.rs b/src/mocks/zitadel.rs index b97fb72..4e59ca5 100644 --- a/src/mocks/zitadel.rs +++ b/src/mocks/zitadel.rs @@ -1,4 +1,3 @@ - use super::{new_expectation_store, ExpectationStore}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -405,10 +404,7 @@ impl MockZitadel { Mock::given(method("GET")) .and(path("/oidc/v1/userinfo")) - .and(header( - "authorization", - format!("Bearer {token}").as_str(), - )) + .and(header("authorization", format!("Bearer {token}").as_str())) .respond_with(ResponseTemplate::new(200).set_body_json(&response)) .mount(&self.server) .await; @@ -663,8 +659,8 @@ mod tests { client_id: Some("client".to_string()), username: Some("user@test.com".to_string()), token_type: Some("Bearer".to_string()), - exp: Some(1234567890), - iat: Some(1234567800), + exp: Some(1_234_567_890), + iat: Some(1_234_567_800), sub: Some("user-id".to_string()), aud: Some("audience".to_string()), iss: Some("issuer".to_string()), diff --git a/src/services/browser_service.rs b/src/services/browser_service.rs index 180c6e5..e732913 100644 --- a/src/services/browser_service.rs +++ b/src/services/browser_service.rs @@ -1,4 +1,3 @@ - use anyhow::{Context, Result}; use log::{info, warn}; use std::process::{Child, Command, Stdio}; @@ -169,6 +168,7 @@ impl BrowserService { self.port } + #[allow(clippy::unused_async)] pub async fn stop(&mut self) -> Result<()> { if let Some(mut process) = self.process.take() { info!("Stopping browser"); diff --git a/src/services/minio.rs b/src/services/minio.rs index 1dcb184..204291a 100644 --- a/src/services/minio.rs +++ b/src/services/minio.rs @@ -1,4 +1,3 @@ - use super::{check_tcp_port, ensure_dir, wait_for, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT}; use anyhow::{Context, Result}; use nix::sys::signal::{kill, Signal}; @@ -28,7 +27,10 @@ impl MinioService { if let Ok(stack_path) = std::env::var("BOTSERVER_STACK_PATH") { let minio_path = PathBuf::from(&stack_path).join("bin/drive/minio"); if minio_path.exists() { - log::info!("Using MinIO from BOTSERVER_STACK_PATH: {minio_path:?}"); + log::info!( + "Using MinIO from BOTSERVER_STACK_PATH: {}", + minio_path.display() + ); return Ok(minio_path); } } @@ -43,7 +45,7 @@ impl MinioService { for rel_path in &relative_paths { let minio_path = cwd.join(rel_path); if minio_path.exists() { - log::info!("Using MinIO from botserver-stack: {minio_path:?}"); + log::info!("Using MinIO from botserver-stack: {}", minio_path.display()); return Ok(minio_path); } } @@ -58,13 +60,13 @@ impl MinioService { for path in &system_paths { let minio_path = PathBuf::from(path); if minio_path.exists() { - log::info!("Using system MinIO from: {minio_path:?}"); + log::info!("Using system MinIO from: {}", minio_path.display()); return Ok(minio_path); } } if let Ok(minio_path) = which::which("minio") { - log::info!("Using MinIO from PATH: {minio_path:?}"); + log::info!("Using MinIO from PATH: {}", minio_path.display()); return Ok(minio_path); } @@ -73,7 +75,7 @@ impl MinioService { pub async fn start(api_port: u16, data_dir: &str) -> Result { let bin_path = Self::find_minio_binary()?; - log::info!("Using MinIO from: {bin_path:?}"); + log::info!("Using MinIO from: {}", bin_path.display()); let data_path = PathBuf::from(data_dir).join("minio"); ensure_dir(&data_path)?; @@ -90,7 +92,7 @@ impl MinioService { secret_key: Self::DEFAULT_SECRET_KEY.to_string(), }; - service.start_server().await?; + service.start_server()?; service.wait_ready().await?; Ok(service) @@ -103,7 +105,7 @@ impl MinioService { secret_key: &str, ) -> Result { let bin_path = Self::find_minio_binary()?; - log::info!("Using MinIO from: {bin_path:?}"); + log::info!("Using MinIO from: {}", bin_path.display()); let data_path = PathBuf::from(data_dir).join("minio"); ensure_dir(&data_path)?; @@ -120,13 +122,13 @@ impl MinioService { secret_key: secret_key.to_string(), }; - service.start_server().await?; + service.start_server()?; service.wait_ready().await?; Ok(service) } - async fn start_server(&mut self) -> Result<()> { + fn start_server(&mut self) -> Result<()> { log::info!( "Starting MinIO on port {} (console: {})", self.api_port, @@ -192,11 +194,7 @@ impl MinioService { .output(); let output = Command::new(&mc) - .args([ - "mb", - "--ignore-existing", - &format!("{alias_name}/{name}"), - ]) + .args(["mb", "--ignore-existing", &format!("{alias_name}/{name}")]) .output()?; if !output.status.success() { diff --git a/src/services/redis.rs b/src/services/redis.rs index 150a60b..dd9780c 100644 --- a/src/services/redis.rs +++ b/src/services/redis.rs @@ -1,4 +1,3 @@ - use super::{check_tcp_port, ensure_dir, wait_for, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT}; use anyhow::{Context, Result}; use nix::sys::signal::{kill, Signal}; @@ -50,6 +49,7 @@ impl RedisService { Ok(service) } + #[allow(clippy::unused_async)] async fn start_server(&mut self) -> Result<()> { log::info!("Starting Redis on port {}", self.port); @@ -125,6 +125,7 @@ impl RedisService { Ok(()) } + #[allow(clippy::unused_async)] pub async fn execute(&self, args: &[&str]) -> Result { let redis_cli = Self::find_cli_binary()?; @@ -182,7 +183,10 @@ impl RedisService { if result.is_empty() || result == "(empty list or set)" { Ok(Vec::new()) } else { - Ok(result.lines().map(std::string::ToString::to_string).collect()) + Ok(result + .lines() + .map(std::string::ToString::to_string) + .collect()) } } @@ -193,10 +197,7 @@ impl RedisService { pub async fn publish(&self, channel: &str, message: &str) -> Result { let result = self.execute(&["PUBLISH", channel, message]).await?; - let count = result - .replace("(integer) ", "") - .parse::() - .unwrap_or(0); + let count = result.replace("(integer) ", "").parse::().unwrap_or(0); Ok(count) } @@ -230,10 +231,7 @@ impl RedisService { pub async fn llen(&self, key: &str) -> Result { let result = self.execute(&["LLEN", key]).await?; - let len = result - .replace("(integer) ", "") - .parse::() - .unwrap_or(0); + let len = result.replace("(integer) ", "").parse::().unwrap_or(0); Ok(len) } @@ -271,19 +269,13 @@ impl RedisService { pub async fn incr(&self, key: &str) -> Result { let result = self.execute(&["INCR", key]).await?; - let val = result - .replace("(integer) ", "") - .parse::() - .unwrap_or(0); + let val = result.replace("(integer) ", "").parse::().unwrap_or(0); Ok(val) } pub async fn decr(&self, key: &str) -> Result { let result = self.execute(&["DECR", key]).await?; - let val = result - .replace("(integer) ", "") - .parse::() - .unwrap_or(0); + let val = result.replace("(integer) ", "").parse::().unwrap_or(0); Ok(val) } diff --git a/src/web/mod.rs b/src/web/mod.rs index ba326ef..f9d8a45 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,4 +1,3 @@ - pub mod browser; pub mod pages; @@ -108,6 +107,7 @@ impl Locator { } #[must_use] + #[allow(clippy::match_same_arms)] pub fn to_css_selector(&self) -> String { match self { Self::Css(s) => s.clone(), diff --git a/tests/e2e/auth_flow.rs b/tests/e2e/auth_flow.rs index 6d8fdb9..530af21 100644 --- a/tests/e2e/auth_flow.rs +++ b/tests/e2e/auth_flow.rs @@ -156,11 +156,9 @@ async fn perform_logout(browser: &Browser, base_url: &str) -> Result Result Result<()> { if !should_run_e2e_tests() { eprintln!("Skipping: E2E tests disabled"); - return; + return Ok(()); } - let ctx = match E2ETestContext::setup_with_browser().await { - Ok(ctx) => ctx, - Err(e) => { - eprintln!("Test failed: {}", e); - panic!("Failed to setup E2E context: {}", e); - } - }; + let ctx = E2ETestContext::setup_with_browser().await?; if !ctx.has_browser() { ctx.close().await; - panic!("Browser not available - cannot run E2E test"); + bail!("Browser not available - cannot run E2E test"); } if ctx.ui.is_none() { ctx.close().await; - panic!("BotUI not available - chat tests require botui running on port 3000"); + bail!("BotUI not available - chat tests require botui running on port 3000"); } let browser = ctx.browser.as_ref().unwrap(); let ui_url = ctx.ui.as_ref().unwrap().url.clone(); let chat_url = format!("{}/#chat", ui_url); - println!("🌐 Navigating to: {}", chat_url); + println!("🌐 Navigating to: {chat_url}"); if let Err(e) = browser.goto(&chat_url).await { ctx.close().await; - panic!("Failed to navigate to chat: {}", e); + bail!("Failed to navigate to chat: {e}"); } println!("ā³ Waiting for page to load..."); @@ -47,10 +42,10 @@ async fn test_chat_hi() { for attempt in 1..=10 { if browser.exists(input.clone()).await { found_input = true; - println!("āœ“ Chat input found (attempt {})", attempt); + println!("āœ“ Chat input found (attempt {attempt})"); break; } - println!(" ... waiting for chat input (attempt {}/10)", attempt); + println!(" ... waiting for chat input (attempt {attempt}/10)"); tokio::time::sleep(std::time::Duration::from_secs(1)).await; } @@ -61,27 +56,25 @@ async fn test_chat_hi() { } if let Ok(source) = browser.page_source().await { let preview: String = source.chars().take(2000).collect(); - println!("Page source preview:\n{}", preview); + println!("Page source preview:\n{preview}"); } ctx.close().await; - panic!("Chat input not found after 10 attempts"); + bail!("Chat input not found after 10 attempts"); } println!("āŒØļø Typing 'hi'..."); if let Err(e) = browser.type_text(input.clone(), "hi").await { ctx.close().await; - panic!("Failed to type: {}", e); + bail!("Failed to type: {e}"); } let send_btn = Locator::css("#sendBtn, #ai-send, .ai-send, button[type='submit']"); match browser.click(send_btn).await { - Ok(_) => println!("āœ“ Message sent (click)"), - Err(_) => { - match browser.press_key(input, "Enter").await { - Ok(_) => println!("āœ“ Message sent (Enter key)"), - Err(e) => println!("⚠ Send may have failed: {}", e), - } - } + Ok(()) => println!("āœ“ Message sent (click)"), + Err(_) => match browser.press_key(input, "Enter").await { + Ok(()) => println!("āœ“ Message sent (Enter key)"), + Err(e) => println!("⚠ Send may have failed: {e}"), + }, } println!("ā³ Waiting for bot response..."); @@ -105,29 +98,25 @@ async fn test_chat_hi() { ctx.close().await; println!("āœ… Chat test complete!"); + Ok(()) } #[tokio::test] -async fn test_chat_page_loads() { +async fn test_chat_page_loads() -> Result<()> { if !should_run_e2e_tests() { - return; + return Ok(()); } - let ctx = match E2ETestContext::setup_with_browser().await { - Ok(ctx) => ctx, - Err(e) => { - panic!("Setup failed: {}", e); - } - }; + let ctx = E2ETestContext::setup_with_browser().await?; if !ctx.has_browser() { ctx.close().await; - panic!("Browser not available"); + bail!("Browser not available"); } if ctx.ui.is_none() { ctx.close().await; - panic!("BotUI not available - chat tests require botui. Start it with: cd ../botui && cargo run"); + bail!("BotUI not available - chat tests require botui. Start it with: cd ../botui && cargo run"); } let browser = ctx.browser.as_ref().unwrap(); @@ -136,7 +125,7 @@ async fn test_chat_page_loads() { if let Err(e) = browser.goto(&chat_url).await { ctx.close().await; - panic!("Navigation failed: {}", e); + bail!("Navigation failed: {e}"); } tokio::time::sleep(std::time::Duration::from_secs(1)).await; @@ -149,9 +138,10 @@ async fn test_chat_page_loads() { let _ = std::fs::write("/tmp/bottest-fail.png", &s); } ctx.close().await; - panic!("Chat not loaded: {}", e); + bail!("Chat not loaded: {e}"); } } ctx.close().await; + Ok(()) } diff --git a/tests/e2e/dashboard.rs b/tests/e2e/dashboard.rs index 75b95ff..2598cc7 100644 --- a/tests/e2e/dashboard.rs +++ b/tests/e2e/dashboard.rs @@ -773,7 +773,10 @@ async fn test_with_fixtures() { match ctx.ctx.insert_user(&user).await { Ok(_) => println!("Inserted test user: {}", user.email), - Err(e) => eprintln!("Could not insert user (DB may not be directly accessible): {}", e), + Err(e) => eprintln!( + "Could not insert user (DB may not be directly accessible): {}", + e + ), } match ctx.ctx.insert_bot(&bot).await { @@ -822,12 +825,10 @@ async fn test_mock_services_available() { Ok(_pool) => println!("āœ“ Connected to existing PostgreSQL"), Err(e) => eprintln!("Could not connect to existing PostgreSQL: {}", e), } + } else if ctx.ctx.postgres().is_some() { + println!("āœ“ PostgreSQL is managed by harness"); } else { - if ctx.ctx.postgres().is_some() { - println!("āœ“ PostgreSQL is managed by harness"); - } else { - eprintln!("PostgreSQL should be started in fresh stack mode"); - } + eprintln!("PostgreSQL should be started in fresh stack mode"); } ctx.close().await; diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs index 8698ddc..2bc5447 100644 --- a/tests/e2e/mod.rs +++ b/tests/e2e/mod.rs @@ -23,7 +23,7 @@ async fn is_service_running(url: &str) -> bool { .build() .unwrap_or_default(); - if let Ok(resp) = client.get(&format!("{}/health", url)).send().await { + if let Ok(resp) = client.get(format!("{url}/health")).send().await { if resp.status().is_success() { return true; } diff --git a/tests/integration/api.rs b/tests/integration/api.rs index db203be..0c9f3f6 100644 --- a/tests/integration/api.rs +++ b/tests/integration/api.rs @@ -36,25 +36,6 @@ async fn get_test_server() -> Option<(Option, String)> { } } -fn is_server_available_sync() -> bool { - if std::env::var("SKIP_INTEGRATION_TESTS").is_ok() { - return false; - } - - if let Some(url) = external_server_url() { - let client = reqwest::blocking::Client::builder() - .timeout(Duration::from_secs(2)) - .build() - .ok(); - - if let Some(client) = client { - return client.get(&url).send().is_ok(); - } - } - - false -} - macro_rules! skip_if_no_server { ($base_url:expr) => { if $base_url.is_none() { @@ -658,7 +639,7 @@ async fn test_mock_llm_assertions() { let client = reqwest::Client::new(); let _ = client - .post(&format!("{}/v1/chat/completions", mock_llm.url())) + .post(format!("{}/v1/chat/completions", mock_llm.url())) .json(&serde_json::json!({ "model": "gpt-4", "messages": [{"role": "user", "content": "test"}] @@ -685,7 +666,7 @@ async fn test_mock_llm_error_simulation() { let client = reqwest::Client::new(); let response = client - .post(&format!("{}/v1/chat/completions", mock_llm.url())) + .post(format!("{}/v1/chat/completions", mock_llm.url())) .json(&serde_json::json!({ "model": "gpt-4", "messages": [{"role": "user", "content": "test"}] diff --git a/tests/integration/basic_runtime.rs b/tests/integration/basic_runtime.rs index bae502e..005366f 100644 --- a/tests/integration/basic_runtime.rs +++ b/tests/integration/basic_runtime.rs @@ -1,7 +1,6 @@ use rhai::Engine; use std::sync::{Arc, Mutex}; - fn create_basic_engine() -> Engine { let mut engine = Engine::new(); @@ -135,9 +134,8 @@ impl InputProvider { fn create_conversation_engine(output: OutputCollector, input: InputProvider) -> Engine { let mut engine = create_basic_engine(); - let output_clone = output.clone(); engine.register_fn("TALK", move |msg: &str| { - output_clone.add_message(msg.to_string()); + output.add_message(msg.to_string()); }); engine.register_fn("HEAR", move || -> String { input.next_input() }); @@ -145,7 +143,6 @@ fn create_conversation_engine(output: OutputCollector, input: InputProvider) -> engine } - #[test] fn test_string_concatenation_in_engine() { let engine = create_basic_engine(); @@ -208,7 +205,6 @@ fn test_replace_function() { assert_eq!(result, "bbb"); } - #[test] fn test_math_operations_chain() { let engine = create_basic_engine(); @@ -264,14 +260,13 @@ fn test_val_function() { let result: f64 = engine.eval(r#"VAL("42")"#).unwrap(); assert!((result - 42.0).abs() < f64::EPSILON); - let result: f64 = engine.eval(r#"VAL("3.14")"#).unwrap(); - assert!((result - 3.14).abs() < f64::EPSILON); + let result: f64 = engine.eval(r#"VAL("3.5")"#).unwrap(); + assert!((result - 3.5).abs() < f64::EPSILON); let result: f64 = engine.eval(r#"VAL("invalid")"#).unwrap(); assert!((result - 0.0).abs() < f64::EPSILON); } - #[test] fn test_talk_output() { let output = OutputCollector::new(); @@ -393,19 +388,18 @@ fn test_keyword_detection() { assert_eq!(messages[0], "I can help you! What do you need?"); } - #[test] fn test_variable_assignment() { let engine = create_basic_engine(); let result: i64 = engine .eval( - r#" + r" let x = 10; let y = 20; let z = x + y; z - "#, + ", ) .unwrap(); assert_eq!(result, 30); @@ -442,7 +436,6 @@ fn test_numeric_expressions() { assert_eq!(result, 12); } - #[test] fn test_for_loop() { let output = OutputCollector::new(); @@ -472,7 +465,7 @@ fn test_while_loop() { let result: i64 = engine .eval( - r#" + r" let count = 0; let sum = 0; while count < 5 { @@ -480,21 +473,19 @@ fn test_while_loop() { count = count + 1; } sum - "#, + ", ) .unwrap(); assert_eq!(result, 10); } - #[test] fn test_division_by_zero() { let engine = create_basic_engine(); let result = engine.eval::("10.0 / 0.0"); - match result { - Ok(val) => assert!(val.is_infinite() || val.is_nan()), - Err(_) => (), + if let Ok(val) = result { + assert!(val.is_infinite() || val.is_nan()); } } @@ -514,7 +505,6 @@ fn test_type_mismatch() { assert!(result.is_err()); } - #[test] fn test_greeting_script_logic() { let output = OutputCollector::new(); @@ -608,7 +598,6 @@ fn test_echo_bot_logic() { assert_eq!(messages[2], "You said: How are you?"); } - #[test] fn test_order_lookup_simulation() { let output = OutputCollector::new(); diff --git a/tests/integration/database.rs b/tests/integration/database.rs index f0c98b6..bfaa80d 100644 --- a/tests/integration/database.rs +++ b/tests/integration/database.rs @@ -154,37 +154,37 @@ async fn test_query_result_types() { use diesel::sql_query; #[derive(QueryableByName)] - struct TypeTestResult { + struct TypeTestRow { #[diesel(sql_type = diesel::sql_types::Integer)] - int_val: i32, + integer: i32, #[diesel(sql_type = diesel::sql_types::BigInt)] - bigint_val: i64, + bigint: i64, #[diesel(sql_type = diesel::sql_types::Text)] - text_val: String, + text: String, #[diesel(sql_type = diesel::sql_types::Bool)] - bool_val: bool, + flag: bool, #[diesel(sql_type = diesel::sql_types::Double)] - float_val: f64, + decimal: f64, } let mut conn = pool.get().expect("Failed to get connection"); - let result: Vec = sql_query( + let result: Vec = sql_query( "SELECT - 42 as int_val, - 9223372036854775807::bigint as bigint_val, - 'hello' as text_val, - true as bool_val, - 3.125 as float_val", + 42 as integer, + 9223372036854775807::bigint as bigint, + 'hello' as text, + true as flag, + 3.125 as decimal", ) .load(&mut conn) .expect("Query failed"); assert_eq!(result.len(), 1); - assert_eq!(result[0].int_val, 42); - assert_eq!(result[0].bigint_val, 9223372036854775807_i64); - assert_eq!(result[0].text_val, "hello"); - assert!(result[0].bool_val); - assert!((result[0].float_val - 3.125).abs() < 0.0001); + assert_eq!(result[0].integer, 42); + assert_eq!(result[0].bigint, 9_223_372_036_854_775_807_i64); + assert_eq!(result[0].text, "hello"); + assert!(result[0].flag); + assert!((result[0].decimal - 3.125).abs() < 0.0001); } #[tokio::test] diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index b082869..5cdd685 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -29,16 +29,6 @@ pub fn should_run_integration_tests() -> bool { true } -#[macro_export] -macro_rules! skip_if_no_services { - () => { - if !crate::integration::should_run_integration_tests() { - eprintln!("Skipping integration test: SKIP_INTEGRATION_TESTS is set"); - return; - } - }; -} - #[tokio::test] async fn test_harness_database_only() { if !should_run_integration_tests() { diff --git a/tests/unit/mod.rs b/tests/unit/mod.rs index 908d0ae..cd95dcb 100644 --- a/tests/unit/mod.rs +++ b/tests/unit/mod.rs @@ -1,6 +1,5 @@ #[test] fn test_unit_module_loads() { - // Unit tests are now inline in botserver source files - // This module is kept for integration test infrastructure - assert!(true); + let module_name = module_path!(); + assert!(module_name.contains("unit")); }