search_hub

at f4eeb1f Raw

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);
}