AkurAI Build
Menu

Public repository / master

AkurAI-Notes / crates/notes-api/tests/api_test.rs

Latest change b94d307489644d24c8963b0c4140a2c514d03d36 - Fix clippy/test for CI gate by Ólafur Búi Ólafsson

use akurai_http::Request;
use akurai_json::Value;
use notes_api::auth::idp_client::IdpConfig;
use notes_api::{build_router, NotesState};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

fn setup_db(name: &str) -> (NotesState, String) {
    let db_path = format!("_test_{}.db", name);
    let _ = std::fs::remove_file(&db_path);
    let frontend = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../site/frontend");
    let state = NotesState::new(&db_path, frontend, IdpConfig::from_env()).unwrap();
    (state, db_path)
}

fn cleanup(db_path: &str) {
    let _ = std::fs::remove_file(db_path);
}

fn request(path: &str) -> Request {
    let raw = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n");
    Request::parse_head(raw.as_bytes()).unwrap()
}

#[test]
fn test_list_notes_returns_only_owned_notes() {
    let (state, db_path) = setup_db("list_owned");
    let mut db = state.db.lock().unwrap();

    fn make_key(coll: &str, id: u64) -> Vec<u8> {
        let mut k = format!("{}:", coll).into_bytes();
        k.extend_from_slice(&id.to_be_bytes());
        k
    }

    let now = NotesState::now();

    // Insert a note owned by olibuijr@olibuijr.com
    let note1 = Value::Object(vec![
        ("id".into(), Value::Int(1)),
        ("title".into(), Value::Str("My note".into())),
        ("body".into(), Value::Str("Body 1".into())),
        (
            "ownerSub".into(),
            Value::Str("olibuijr@olibuijr.com".into()),
        ),
        ("createdAt".into(), Value::Int(now)),
        ("updatedAt".into(), Value::Int(now)),
    ]);
    db.insert(&make_key("notes", 1), note1.to_json().as_bytes())
        .unwrap();

    // Insert a note owned by other@example.com
    let note2 = Value::Object(vec![
        ("id".into(), Value::Int(2)),
        ("title".into(), Value::Str("Other's note".into())),
        ("body".into(), Value::Str("Body 2".into())),
        ("ownerSub".into(), Value::Str("other@example.com".into())),
        ("createdAt".into(), Value::Int(now)),
        ("updatedAt".into(), Value::Int(now)),
    ]);
    db.insert(&make_key("notes", 2), note2.to_json().as_bytes())
        .unwrap();
    db.commit().unwrap();
    drop(db);

    // Query through the state (simulated owner filter)
    let mut db = state.db.lock().unwrap();
    let prefix = b"notes:";
    let mut end = prefix.to_vec();
    end.push(0xff);

    let mut owned = Vec::new();
    if let Ok(entries) = db.range(prefix, &end) {
        for (_key, val) in entries {
            if let Ok(json) = akurai_json::parse(std::str::from_utf8(&val).unwrap_or("")) {
                if json.get("ownerSub").and_then(|v| v.as_str()) == Some("olibuijr@olibuijr.com") {
                    owned.push(json);
                }
            }
        }
    }

    assert_eq!(owned.len(), 1, "should only find the owned note");
    assert_eq!(
        owned[0].get("title").and_then(|v| v.as_str()),
        Some("My note")
    );

    cleanup(&db_path);
}

#[test]
fn test_create_note_inserts_with_owner() {
    let (state, db_path) = setup_db("create_owner");
    let mut db = state.db.lock().unwrap();

    fn make_key(coll: &str, id: u64) -> Vec<u8> {
        let mut k = format!("{}:", coll).into_bytes();
        k.extend_from_slice(&id.to_be_bytes());
        k
    }

    let now = NotesState::now();

    let record = Value::Object(vec![
        ("id".into(), Value::Int(1)),
        ("title".into(), Value::Str("Test note".into())),
        ("body".into(), Value::Str("Test body".into())),
        (
            "ownerSub".into(),
            Value::Str("olibuijr@olibuijr.com".into()),
        ),
        ("format".into(), Value::Str("markdown".into())),
        ("notebookId".into(), Value::Int(1)),
        ("tagIds".into(), Value::Str("1,2".into())),
        ("createdAt".into(), Value::Int(now)),
        ("updatedAt".into(), Value::Int(now)),
    ]);

    db.insert(&make_key("notes", 1), record.to_json().as_bytes())
        .unwrap();
    db.commit().unwrap();

    let got = db.get(&make_key("notes", 1)).unwrap().unwrap();
    let got_str = String::from_utf8_lossy(&got);
    assert!(got_str.contains("Test note"));
    assert!(got_str.contains("olibuijr@olibuijr.com"));
    assert!(got_str.contains("1,2"));

    cleanup(&db_path);
}

#[test]
fn app_search_route_renders_template() {
    let (state, db_path) = setup_db("app_search_route");
    let routes = build_router(Arc::new(Mutex::new(state)));
    let route = routes
        .iter()
        .find(|route| route.method == "GET" && route.pattern == "/app/search")
        .expect("search SSR route should be registered");

    let response = (route.handler)(&request("/app/search?q=meeting"));
    let body = String::from_utf8(response.body).unwrap();

    assert_eq!(response.status, 200, "body was: {body}");
    assert!(body.contains("/app#all"), "body was: {body}");
    assert!(!body.contains("{%"), "template block leaked: {body}");
    assert!(!body.contains("{{"), "template variable leaked: {body}");

    cleanup(&db_path);
}

#[test]
fn static_fallback_does_not_serve_raw_templates() {
    let (state, db_path) = setup_db("static_fallback");
    let state = Arc::new(Mutex::new(state));
    let handler = notes_api::handlers::static_files::static_file_route(state);

    let response = handler(&request("/app/missing"));
    let body = String::from_utf8(response.body).unwrap();

    assert_eq!(response.status, 404);
    assert!(!body.contains("{%"), "raw template block leaked: {body}");
    assert!(!body.contains("{{"), "raw template variable leaked: {body}");

    cleanup(&db_path);
}