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