use actix_web::{test, web, App}; use chrono::Utc; use search_hub::config::{EngineConfig, Shortcut}; use search_hub::models::Bookmark; use search_hub::storage; use search_hub::web::{handlers, DbPool, SearchApiResponse, ServerConfig}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use tera::Tera; macro_rules! setup { ($path:expr) => {{ let db_pool = web::Data::new(DbPool::new($path)); let engines = web::Data::new(Arc::new(RwLock::new(Vec::<EngineConfig>::new()))); let cfg = web::Data::new(ServerConfig { port: 8080, bind_address: "127.0.0.1".into(), page_size: 20, workers: 1, }); let shortcuts = web::Data::new(HashMap::<String, Shortcut>::new()); let mut tera_ = Tera::default(); tera_ .add_raw_template("index.html", include_str!("../templates/index.html")) .expect("template parse"); let tera = web::Data::new(tera_); test::init_service( App::new() .app_data(tera) .app_data(db_pool) .app_data(engines) .app_data(cfg) .app_data(shortcuts) .service(handlers::index) .service(handlers::search) .service(handlers::api_search), ) .await }}; } fn insert_bookmark(conn: &rusqlite::Connection, title: &str, url: &str, content: &str) { storage::insert_bookmark( conn, &Bookmark { id: 0, title: title.into(), url: url.into(), description: None, source: "bookmark".into(), content: Some(content.into()), tags: None, created_at: Utc::now(), }, ) .expect("insert bookmark"); } fn count_rows(conn: &rusqlite::Connection) -> usize { storage::list_bookmarks(conn, 1, 1000).unwrap_or_default().len() } #[actix_web::test] async fn api_search_returns_matching_bookmarks() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let pool = DbPool::new(&path); let conn = pool.conn(); insert_bookmark(&conn, "Rust programming", "https://rust-lang.org", "Rust is great"); insert_bookmark(&conn, "Python tutorial", "https://python.org", "Python is fun"); assert_eq!(count_rows(&conn), 2); let req = test::TestRequest::get() .uri("/api/search?q=rust") .to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let body: SearchApiResponse = serde_json::from_slice(&test::read_body(resp).await).unwrap(); assert_eq!(body.query, "rust"); assert_eq!(body.page, 1); assert_eq!(body.total_results, 1); assert_eq!(body.results.len(), 1); assert_eq!(body.results[0].title(), "Rust programming"); } #[actix_web::test] async fn api_search_no_match_returns_empty() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let pool = DbPool::new(&path); let conn = pool.conn(); insert_bookmark(&conn, "Rust programming", "https://rust-lang.org", "Rust is great"); let req = test::TestRequest::get() .uri("/api/search?q=nonexistent") .to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let body: SearchApiResponse = serde_json::from_slice(&test::read_body(resp).await).unwrap(); assert_eq!(body.total_results, 0); assert!(body.results.is_empty()); } #[actix_web::test] async fn api_search_empty_query_returns_all() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let pool = DbPool::new(&path); let conn = pool.conn(); insert_bookmark(&conn, "Alpha", "https://a.com", "first"); insert_bookmark(&conn, "Beta", "https://b.com", "second"); assert_eq!(count_rows(&conn), 2); let req = test::TestRequest::get().uri("/api/search").to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let body: SearchApiResponse = serde_json::from_slice(&test::read_body(resp).await).unwrap(); assert_eq!(body.total_results, 2); assert_eq!(body.results.len(), 2); } #[actix_web::test] async fn html_search_renders_matching_bookmarks() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let pool = DbPool::new(&path); let conn = pool.conn(); insert_bookmark( &conn, "Rust programming", "https://rust-lang.org", "Rust is a systems language", ); let req = test::TestRequest::get() .uri("/search?q=rust") .to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let html = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); assert!(html.contains("Rust programming")); assert!(html.contains("rust-lang.org")); } #[actix_web::test] async fn html_search_no_match_shows_no_results() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let pool = DbPool::new(&path); let conn = pool.conn(); insert_bookmark(&conn, "Rust", "https://rust-lang.org", "Rust is great"); let req = test::TestRequest::get() .uri("/search?q=nope") .to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let html = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); assert!(html.contains("no bookmarks found")); } #[actix_web::test] async fn html_search_empty_query_shows_prompt() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let req = test::TestRequest::get().uri("/search").to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let html = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); assert!(html.contains("Bookmark Search")); assert!(html.contains("enter a query")); } #[actix_web::test] async fn index_page_renders() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let req = test::TestRequest::get().uri("/").to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let html = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); assert!(html.contains("Bookmark Search")); } #[actix_web::test] async fn api_search_pagination() { let tmp = tempfile::NamedTempFile::new().unwrap(); let path = tmp.path().to_str().unwrap().to_string(); let app = setup!(&path); let pool = DbPool::new(&path); let conn = pool.conn(); for i in 0..5 { insert_bookmark( &conn, &format!("Item {i}"), &format!("https://example.com/{i}"), "content", ); } assert_eq!(count_rows(&conn), 5); let req = test::TestRequest::get() .uri("/api/search?q=item&page=1&page_size=2") .to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let body: SearchApiResponse = serde_json::from_slice(&test::read_body(resp).await).unwrap(); assert_eq!(body.total_results, 5); assert_eq!(body.page_size, 2); assert_eq!(body.results.len(), 2); }