2025-12-06 11:05:57 -03:00
|
|
|
mod auth_flow;
|
|
|
|
|
mod chat;
|
|
|
|
|
mod dashboard;
|
2025-12-06 11:15:14 -03:00
|
|
|
mod platform_flow;
|
2025-12-06 11:05:57 -03:00
|
|
|
|
|
|
|
|
use bottest::prelude::*;
|
2025-12-07 02:14:37 -03:00
|
|
|
use bottest::services::ChromeDriverService;
|
2025-12-06 11:05:57 -03:00
|
|
|
use bottest::web::{Browser, BrowserConfig, BrowserType};
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2025-12-07 02:14:37 -03:00
|
|
|
static CHROMEDRIVER_PORT: u16 = 4444;
|
|
|
|
|
|
2025-12-06 11:05:57 -03:00
|
|
|
pub struct E2ETestContext {
|
|
|
|
|
pub ctx: TestContext,
|
|
|
|
|
pub server: BotServerInstance,
|
2025-12-14 15:59:07 -03:00
|
|
|
pub ui: Option<BotUIInstance>,
|
2025-12-06 11:05:57 -03:00
|
|
|
pub browser: Option<Browser>,
|
2025-12-07 02:14:37 -03:00
|
|
|
chromedriver: Option<ChromeDriverService>,
|
2025-12-06 11:05:57 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl E2ETestContext {
|
|
|
|
|
pub async fn setup() -> anyhow::Result<Self> {
|
2025-12-14 16:40:07 -03:00
|
|
|
// Default to USE_EXISTING_STACK for faster e2e tests
|
|
|
|
|
// Set FULL_BOOTSTRAP=1 to run full bootstrap instead
|
2025-12-14 16:44:29 -03:00
|
|
|
let use_existing = std::env::var("FULL_BOOTSTRAP").is_err();
|
|
|
|
|
|
|
|
|
|
let (ctx, server, ui) = if use_existing {
|
|
|
|
|
// Use existing stack - connect to running botserver/botui
|
|
|
|
|
// Make sure they are running:
|
|
|
|
|
// cargo run --package botserver
|
|
|
|
|
// BOTSERVER_URL=https://localhost:8080 cargo run --package botui
|
2025-12-14 16:40:07 -03:00
|
|
|
log::info!("Using existing stack (set FULL_BOOTSTRAP=1 for full bootstrap)");
|
2025-12-14 16:44:29 -03:00
|
|
|
let ctx = TestHarness::with_existing_stack().await?;
|
|
|
|
|
|
|
|
|
|
// Get URLs from env or use defaults
|
|
|
|
|
let botserver_url = std::env::var("BOTSERVER_URL")
|
|
|
|
|
.unwrap_or_else(|_| "https://localhost:8080".to_string());
|
|
|
|
|
let botui_url =
|
|
|
|
|
std::env::var("BOTUI_URL").unwrap_or_else(|_| "http://localhost:3000".to_string());
|
2025-12-06 11:05:57 -03:00
|
|
|
|
2025-12-14 16:44:29 -03:00
|
|
|
// Create a dummy server instance pointing to existing botserver
|
|
|
|
|
let server = BotServerInstance::existing(&botserver_url);
|
|
|
|
|
let ui = Some(BotUIInstance::existing(&botui_url));
|
|
|
|
|
|
|
|
|
|
(ctx, server, ui)
|
|
|
|
|
} else {
|
|
|
|
|
let ctx = TestHarness::full().await?;
|
|
|
|
|
let server = ctx.start_botserver().await?;
|
|
|
|
|
let ui = ctx.start_botui(&server.url).await.ok();
|
|
|
|
|
(ctx, server, ui)
|
|
|
|
|
};
|
2025-12-14 15:59:07 -03:00
|
|
|
|
2025-12-06 11:05:57 -03:00
|
|
|
Ok(Self {
|
|
|
|
|
ctx,
|
|
|
|
|
server,
|
2025-12-14 15:59:07 -03:00
|
|
|
ui,
|
2025-12-06 11:05:57 -03:00
|
|
|
browser: None,
|
2025-12-07 02:14:37 -03:00
|
|
|
chromedriver: None,
|
2025-12-06 11:05:57 -03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn setup_with_browser() -> anyhow::Result<Self> {
|
2025-12-14 16:40:07 -03:00
|
|
|
// Default to USE_EXISTING_STACK for faster e2e tests
|
|
|
|
|
// Set FULL_BOOTSTRAP=1 to run full bootstrap instead
|
2025-12-14 16:44:29 -03:00
|
|
|
let use_existing = std::env::var("FULL_BOOTSTRAP").is_err();
|
|
|
|
|
|
|
|
|
|
let (ctx, server, ui) = if use_existing {
|
|
|
|
|
// Use existing stack - connect to running botserver/botui
|
2025-12-14 16:40:07 -03:00
|
|
|
log::info!("Using existing stack (set FULL_BOOTSTRAP=1 for full bootstrap)");
|
2025-12-14 16:44:29 -03:00
|
|
|
let ctx = TestHarness::with_existing_stack().await?;
|
|
|
|
|
|
|
|
|
|
let botserver_url = std::env::var("BOTSERVER_URL")
|
|
|
|
|
.unwrap_or_else(|_| "https://localhost:8080".to_string());
|
|
|
|
|
let botui_url =
|
|
|
|
|
std::env::var("BOTUI_URL").unwrap_or_else(|_| "http://localhost:3000".to_string());
|
2025-12-06 11:05:57 -03:00
|
|
|
|
2025-12-14 16:44:29 -03:00
|
|
|
let server = BotServerInstance::existing(&botserver_url);
|
|
|
|
|
let ui = Some(BotUIInstance::existing(&botui_url));
|
|
|
|
|
|
|
|
|
|
(ctx, server, ui)
|
|
|
|
|
} else {
|
|
|
|
|
let ctx = TestHarness::full().await?;
|
|
|
|
|
let server = ctx.start_botserver().await?;
|
|
|
|
|
let ui = ctx.start_botui(&server.url).await.ok();
|
|
|
|
|
(ctx, server, ui)
|
|
|
|
|
};
|
2025-12-14 15:59:07 -03:00
|
|
|
|
2025-12-07 02:14:37 -03:00
|
|
|
let chromedriver = match ChromeDriverService::start(CHROMEDRIVER_PORT).await {
|
|
|
|
|
Ok(cd) => Some(cd),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::warn!("Failed to start ChromeDriver: {}", e);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let browser = if chromedriver.is_some() {
|
|
|
|
|
let config = browser_config();
|
|
|
|
|
Browser::new(config).await.ok()
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2025-12-06 11:05:57 -03:00
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
ctx,
|
|
|
|
|
server,
|
2025-12-14 15:59:07 -03:00
|
|
|
ui,
|
2025-12-06 11:05:57 -03:00
|
|
|
browser,
|
2025-12-07 02:14:37 -03:00
|
|
|
chromedriver,
|
2025-12-06 11:05:57 -03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 15:59:07 -03:00
|
|
|
/// Get the base URL for browser tests - uses botui if available, otherwise botserver
|
2025-12-06 11:05:57 -03:00
|
|
|
pub fn base_url(&self) -> &str {
|
2025-12-14 15:59:07 -03:00
|
|
|
if let Some(ref ui) = self.ui {
|
|
|
|
|
&ui.url
|
|
|
|
|
} else {
|
|
|
|
|
&self.server.url
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the botserver API URL
|
|
|
|
|
pub fn api_url(&self) -> &str {
|
2025-12-06 11:05:57 -03:00
|
|
|
&self.server.url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn has_browser(&self) -> bool {
|
|
|
|
|
self.browser.is_some()
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 02:14:37 -03:00
|
|
|
pub async fn close(mut self) {
|
2025-12-06 11:05:57 -03:00
|
|
|
if let Some(browser) = self.browser {
|
|
|
|
|
let _ = browser.close().await;
|
|
|
|
|
}
|
2025-12-07 02:14:37 -03:00
|
|
|
if let Some(mut cd) = self.chromedriver.take() {
|
|
|
|
|
let _ = cd.stop().await;
|
|
|
|
|
}
|
2025-12-06 11:05:57 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn browser_config() -> BrowserConfig {
|
|
|
|
|
let headless = std::env::var("HEADED").is_err();
|
2025-12-07 02:14:37 -03:00
|
|
|
let webdriver_url = std::env::var("WEBDRIVER_URL")
|
|
|
|
|
.unwrap_or_else(|_| format!("http://localhost:{}", CHROMEDRIVER_PORT));
|
|
|
|
|
|
|
|
|
|
// Detect Brave browser path
|
|
|
|
|
let brave_paths = [
|
|
|
|
|
"/usr/bin/brave-browser",
|
|
|
|
|
"/usr/bin/brave",
|
|
|
|
|
"/snap/bin/brave",
|
|
|
|
|
"/opt/brave.com/brave/brave-browser",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let mut config = BrowserConfig::default()
|
2025-12-06 11:05:57 -03:00
|
|
|
.with_browser(BrowserType::Chrome)
|
|
|
|
|
.with_webdriver_url(&webdriver_url)
|
|
|
|
|
.headless(headless)
|
|
|
|
|
.with_timeout(Duration::from_secs(30))
|
2025-12-07 02:14:37 -03:00
|
|
|
.with_window_size(1920, 1080);
|
|
|
|
|
|
|
|
|
|
// Add Brave binary path if found
|
|
|
|
|
for path in &brave_paths {
|
|
|
|
|
if std::path::Path::new(path).exists() {
|
|
|
|
|
log::info!("Using browser binary: {}", path);
|
|
|
|
|
config = config.with_binary(path);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config
|
2025-12-06 11:05:57 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn should_run_e2e_tests() -> bool {
|
|
|
|
|
if std::env::var("SKIP_E2E_TESTS").is_ok() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn check_webdriver_available() -> bool {
|
2025-12-07 02:14:37 -03:00
|
|
|
true
|
2025-12-06 11:05:57 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_e2e_context_setup() {
|
|
|
|
|
if !should_run_e2e_tests() {
|
|
|
|
|
eprintln!("Skipping: E2E tests disabled");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match E2ETestContext::setup().await {
|
|
|
|
|
Ok(ctx) => {
|
|
|
|
|
assert!(!ctx.base_url().is_empty());
|
|
|
|
|
ctx.close().await;
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Skipping: failed to setup E2E context: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_e2e_with_browser() {
|
|
|
|
|
if !should_run_e2e_tests() {
|
|
|
|
|
eprintln!("Skipping: E2E tests disabled");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !check_webdriver_available().await {
|
|
|
|
|
eprintln!("Skipping: WebDriver not available");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match E2ETestContext::setup_with_browser().await {
|
|
|
|
|
Ok(ctx) => {
|
|
|
|
|
if ctx.has_browser() {
|
|
|
|
|
println!("Browser created successfully");
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!("Browser creation failed (WebDriver may not be running)");
|
|
|
|
|
}
|
|
|
|
|
ctx.close().await;
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Skipping: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_harness_starts_server() {
|
|
|
|
|
if !should_run_e2e_tests() {
|
|
|
|
|
eprintln!("Skipping: E2E tests disabled");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ctx = match TestHarness::full().await {
|
|
|
|
|
Ok(ctx) => ctx,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Skipping: {}", e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let server = match ctx.start_botserver().await {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Skipping: {}", e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if server.is_running() {
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
let health_url = format!("{}/health", server.url);
|
|
|
|
|
|
|
|
|
|
if let Ok(resp) = client.get(&health_url).send().await {
|
|
|
|
|
assert!(resp.status().is_success());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_full_harness_has_all_services() {
|
|
|
|
|
let ctx = match TestHarness::full().await {
|
|
|
|
|
Ok(ctx) => ctx,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Skipping: {}", e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-06 14:55:59 -03:00
|
|
|
// Check services that are enabled in full() config
|
|
|
|
|
assert!(ctx.postgres().is_some(), "PostgreSQL should be available");
|
|
|
|
|
assert!(ctx.mock_llm().is_some(), "MockLLM should be available");
|
|
|
|
|
assert!(
|
|
|
|
|
ctx.mock_zitadel().is_some(),
|
|
|
|
|
"MockZitadel should be available"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// MinIO and Redis are disabled in full() config (not in botserver-stack)
|
|
|
|
|
// so we don't assert they are present
|
2025-12-06 11:05:57 -03:00
|
|
|
|
|
|
|
|
assert!(ctx.data_dir.exists());
|
|
|
|
|
assert!(ctx.data_dir.to_str().unwrap().contains("bottest-"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_e2e_cleanup() {
|
|
|
|
|
let mut ctx = match TestHarness::full().await {
|
|
|
|
|
Ok(ctx) => ctx,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Skipping: {}", e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let data_dir = ctx.data_dir.clone();
|
|
|
|
|
assert!(data_dir.exists());
|
|
|
|
|
|
|
|
|
|
ctx.cleanup().await.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(!data_dir.exists());
|
|
|
|
|
}
|