bottest/tests/e2e/auth_flow.rs

587 lines
17 KiB
Rust
Raw Normal View History

2025-12-06 11:05:57 -03:00
use super::{browser_config, check_webdriver_available, should_run_e2e_tests, E2ETestContext};
use bottest::prelude::*;
use bottest::web::WaitCondition;
use bottest::web::{Browser, Locator};
use std::time::Duration;
async fn setup_auth_mocks(ctx: &TestContext, email: &str, password: &str) {
if let Some(mock_zitadel) = ctx.mock_zitadel() {
let user = mock_zitadel.create_test_user(email);
mock_zitadel.expect_login(email, password).await;
mock_zitadel.expect_any_introspect_active().await;
mock_zitadel.expect_any_userinfo().await;
mock_zitadel.expect_revoke().await;
let _ = user;
}
}
async fn setup_chat_mocks(ctx: &TestContext) {
if let Some(mock_llm) = ctx.mock_llm() {
mock_llm
.set_default_response("Hello! I'm your assistant. How can I help you today?")
.await;
mock_llm
.expect_completion("hello", "Hi there! Nice to meet you.")
.await;
mock_llm
.expect_completion(
"help",
"I'm here to help! What do you need assistance with?",
)
.await;
mock_llm
.expect_completion("bye", "Goodbye! Have a great day!")
.await;
}
}
async fn perform_login(
browser: &Browser,
base_url: &str,
email: &str,
password: &str,
) -> Result<bool, String> {
let login_url = format!("{}/login", base_url);
browser
.goto(&login_url)
.await
.map_err(|e| format!("Failed to navigate to login: {}", e))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let email_input = Locator::css("#email, input[name='email'], input[type='email']");
browser
.wait_for(email_input.clone())
.await
.map_err(|e| format!("Email input not found: {}", e))?;
browser
.fill(email_input, email)
.await
.map_err(|e| format!("Failed to fill email: {}", e))?;
let password_input = Locator::css("#password, input[name='password'], input[type='password']");
browser
.fill(password_input, password)
.await
.map_err(|e| format!("Failed to fill password: {}", e))?;
let login_button = Locator::css("#login-button, button[type='submit'], .login-btn, .btn-login");
browser
.click(login_button)
.await
.map_err(|e| format!("Failed to click login: {}", e))?;
tokio::time::sleep(Duration::from_secs(2)).await;
let dashboard_indicators = vec![
".dashboard",
"#dashboard",
"[data-page='dashboard']",
".nav-menu",
".main-content",
".user-menu",
".sidebar",
];
for selector in dashboard_indicators {
let locator = Locator::css(selector);
if browser.exists(locator).await {
return Ok(true);
}
}
let current_url = browser.current_url().await.unwrap_or_default();
if !current_url.contains("/login") {
return Ok(true);
}
Ok(false)
}
async fn send_chat_message(browser: &Browser, message: &str) -> Result<(), String> {
let input_locator = Locator::css(
"#chat-input, .chat-input, textarea[placeholder*='message'], textarea[name='message']",
);
browser
.wait_for(input_locator.clone())
.await
.map_err(|e| format!("Chat input not found: {}", e))?;
browser
.fill(input_locator, message)
.await
.map_err(|e| format!("Failed to type message: {}", e))?;
let send_button = Locator::css("#send-button, .send-button, button[type='submit'], .btn-send");
browser
.click(send_button)
.await
.map_err(|e| format!("Failed to click send: {}", e))?;
tokio::time::sleep(Duration::from_millis(500)).await;
Ok(())
}
async fn wait_for_bot_response(browser: &Browser) -> Result<String, String> {
let response_locator = Locator::css(
".bot-message, .message-bot, .response, .assistant-message, [data-role='assistant']",
);
let element = browser
.wait_for_condition(response_locator, WaitCondition::Present)
.await
.map_err(|e| format!("Bot response not found: {}", e))?;
let text = element
.text()
.await
.map_err(|e| format!("Failed to get response text: {}", e))?;
Ok(text)
}
async fn perform_logout(browser: &Browser, base_url: &str) -> Result<bool, String> {
let logout_selectors = vec![
"#logout-button",
".logout-btn",
"a[href*='logout']",
"button[data-action='logout']",
".user-menu .logout",
"#user-menu-logout",
];
for selector in &logout_selectors {
let locator = Locator::css(selector);
if browser.exists(locator.clone()).await {
if browser.click(locator).await.is_ok() {
tokio::time::sleep(Duration::from_secs(1)).await;
break;
}
}
}
let user_menu_locator = Locator::css(".user-menu, .avatar, .profile-icon, #user-dropdown");
if browser.exists(user_menu_locator.clone()).await {
let _ = browser.click(user_menu_locator).await;
tokio::time::sleep(Duration::from_millis(300)).await;
for selector in &logout_selectors {
let locator = Locator::css(selector);
if browser.exists(locator.clone()).await {
if browser.click(locator).await.is_ok() {
tokio::time::sleep(Duration::from_secs(1)).await;
break;
}
}
}
}
let current_url = browser.current_url().await.unwrap_or_default();
let logged_out = current_url.contains("/login")
|| current_url.contains("/logout")
|| current_url == format!("{}/", base_url)
|| current_url == base_url.to_string();
if logged_out {
return Ok(true);
}
let login_form = Locator::css("#login-form, .login-form, form[action*='login']");
if browser.exists(login_form).await {
return Ok(true);
}
Ok(false)
}
async fn navigate_to_chat(browser: &Browser, base_url: &str, bot_name: &str) -> Result<(), String> {
let chat_url = format!("{}/chat/{}", base_url, bot_name);
browser
.goto(&chat_url)
.await
.map_err(|e| format!("Failed to navigate to chat: {}", e))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let chat_container = Locator::css("#chat-container, .chat-container, .chat-widget, .chat-box");
browser
.wait_for(chat_container)
.await
.map_err(|e| format!("Chat container not found: {}", e))?;
Ok(())
}
#[tokio::test]
async fn test_complete_auth_flow_login_chat_logout() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled");
return;
}
if !check_webdriver_available().await {
eprintln!("Skipping: WebDriver not available");
return;
}
let ctx = match E2ETestContext::setup_with_browser().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
if !ctx.has_browser() {
eprintln!("Skipping: browser not available");
ctx.close().await;
return;
}
let email = "testuser@example.com";
let password = "testpassword123";
let bot_name = "test-bot";
setup_auth_mocks(&ctx.ctx, email, password).await;
setup_chat_mocks(&ctx.ctx).await;
let browser = ctx.browser.as_ref().unwrap();
let base_url = ctx.base_url();
println!("Step 1: Performing login...");
match perform_login(browser, base_url, email, password).await {
Ok(true) => println!(" ✓ Login successful"),
Ok(false) => {
eprintln!(" ✗ Login failed - dashboard not visible");
ctx.close().await;
return;
}
Err(e) => {
eprintln!(" ✗ Login error: {}", e);
ctx.close().await;
return;
}
}
println!("Step 2: Navigating to chat...");
if let Err(e) = navigate_to_chat(browser, base_url, bot_name).await {
eprintln!(" ✗ Navigation error: {}", e);
ctx.close().await;
return;
}
println!(" ✓ Chat page loaded");
println!("Step 3: Sending messages...");
let messages = vec![
("hello", "greeting"),
("I need help", "help request"),
("bye", "farewell"),
];
for (message, description) in messages {
println!(" Sending: {} ({})", message, description);
match send_chat_message(browser, message).await {
Ok(_) => {}
Err(e) => {
eprintln!(" ✗ Failed to send message: {}", e);
continue;
}
}
match wait_for_bot_response(browser).await {
Ok(response) => {
println!(
" ✓ Bot responded: {}...",
&response[..response.len().min(50)]
);
}
Err(e) => {
eprintln!(" ✗ No bot response: {}", e);
}
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
println!("Step 4: Performing logout...");
match perform_logout(browser, base_url).await {
Ok(true) => println!(" ✓ Logout successful"),
Ok(false) => {
eprintln!(" ✗ Logout may have failed - not redirected to login");
}
Err(e) => {
eprintln!(" ✗ Logout error: {}", e);
}
}
println!("Step 5: Verifying logout by attempting to access protected page...");
let dashboard_url = format!("{}/dashboard", base_url);
if browser.goto(&dashboard_url).await.is_ok() {
tokio::time::sleep(Duration::from_secs(1)).await;
let current_url = browser.current_url().await.unwrap_or_default();
if current_url.contains("/login") {
println!(" ✓ Correctly redirected to login page");
} else {
eprintln!(" ✗ Session may still be active");
}
}
println!("\n=== Auth Flow Test Complete ===");
ctx.close().await;
}
#[tokio::test]
async fn test_login_with_invalid_credentials() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled");
return;
}
if !check_webdriver_available().await {
eprintln!("Skipping: WebDriver not available");
return;
}
let ctx = match E2ETestContext::setup_with_browser().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
if !ctx.has_browser() {
eprintln!("Skipping: browser not available");
ctx.close().await;
return;
}
if let Some(mock_zitadel) = ctx.ctx.mock_zitadel() {
mock_zitadel.expect_invalid_credentials().await;
}
let browser = ctx.browser.as_ref().unwrap();
let base_url = ctx.base_url();
match perform_login(browser, base_url, "invalid@test.com", "wrongpassword").await {
Ok(true) => {
eprintln!("✗ Login succeeded with invalid credentials - unexpected");
}
Ok(false) => {
println!("✓ Login correctly rejected invalid credentials");
let error_locator =
Locator::css(".error, .alert-error, .login-error, [role='alert'], .error-message");
if browser.exists(error_locator).await {
println!("✓ Error message displayed to user");
}
}
Err(e) => {
eprintln!("Login attempt failed: {}", e);
}
}
ctx.close().await;
}
#[tokio::test]
async fn test_session_persistence() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled");
return;
}
if !check_webdriver_available().await {
eprintln!("Skipping: WebDriver not available");
return;
}
let ctx = match E2ETestContext::setup_with_browser().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
if !ctx.has_browser() {
eprintln!("Skipping: browser not available");
ctx.close().await;
return;
}
let email = "session@test.com";
let password = "testpass";
setup_auth_mocks(&ctx.ctx, email, password).await;
let browser = ctx.browser.as_ref().unwrap();
let base_url = ctx.base_url();
if perform_login(browser, base_url, email, password)
.await
.unwrap_or(false)
{
println!("✓ Initial login successful");
let dashboard_url = format!("{}/dashboard", base_url);
if browser.goto(&dashboard_url).await.is_ok() {
tokio::time::sleep(Duration::from_secs(1)).await;
}
if browser.refresh().await.is_ok() {
tokio::time::sleep(Duration::from_secs(1)).await;
let current_url = browser.current_url().await.unwrap_or_default();
if !current_url.contains("/login") {
println!("✓ Session persisted after page refresh");
} else {
eprintln!("✗ Session lost after refresh");
}
}
let protected_url = format!("{}/admin/settings", base_url);
if browser.goto(&protected_url).await.is_ok() {
tokio::time::sleep(Duration::from_secs(1)).await;
let current_url = browser.current_url().await.unwrap_or_default();
if !current_url.contains("/login") {
println!("✓ Session maintained across navigation");
} else {
eprintln!("✗ Session lost during navigation");
}
}
} else {
eprintln!("✗ Initial login failed");
}
ctx.close().await;
}
#[tokio::test]
async fn test_chat_message_flow() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled");
return;
}
if !check_webdriver_available().await {
eprintln!("Skipping: WebDriver not available");
return;
}
let ctx = match E2ETestContext::setup_with_browser().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
if !ctx.has_browser() {
eprintln!("Skipping: browser not available");
ctx.close().await;
return;
}
setup_chat_mocks(&ctx.ctx).await;
let browser = ctx.browser.as_ref().unwrap();
let chat_url = format!("{}/chat/test-bot", ctx.base_url());
if browser.goto(&chat_url).await.is_err() {
eprintln!("Failed to navigate to chat");
ctx.close().await;
return;
}
tokio::time::sleep(Duration::from_secs(1)).await;
let message_count_before = browser
.find_all(Locator::css(".message, .chat-message"))
.await
.map(|v| v.len())
.unwrap_or(0);
if send_chat_message(browser, "Hello bot!").await.is_ok() {
tokio::time::sleep(Duration::from_secs(2)).await;
let message_count_after = browser
.find_all(Locator::css(".message, .chat-message"))
.await
.map(|v| v.len())
.unwrap_or(0);
if message_count_after > message_count_before {
println!(
"✓ Messages added to chat: {} -> {}",
message_count_before, message_count_after
);
} else {
eprintln!("✗ No new messages appeared in chat");
}
} else {
eprintln!("✗ Failed to send chat message");
}
ctx.close().await;
}
#[tokio::test]
async fn test_unauthenticated_access_redirect() {
if !should_run_e2e_tests() {
eprintln!("Skipping: E2E tests disabled");
return;
}
if !check_webdriver_available().await {
eprintln!("Skipping: WebDriver not available");
return;
}
let ctx = match E2ETestContext::setup_with_browser().await {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
if !ctx.has_browser() {
eprintln!("Skipping: browser not available");
ctx.close().await;
return;
}
let browser = ctx.browser.as_ref().unwrap();
let base_url = ctx.base_url();
let protected_routes = vec!["/dashboard", "/admin", "/settings", "/profile"];
for route in protected_routes {
let url = format!("{}{}", base_url, route);
if browser.goto(&url).await.is_ok() {
tokio::time::sleep(Duration::from_secs(1)).await;
let current_url = browser.current_url().await.unwrap_or_default();
if current_url.contains("/login") {
println!("{} correctly redirects to login", route);
} else {
eprintln!("{} accessible without authentication", route);
}
}
}
ctx.close().await;
}