50e6ce5ed0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
80 lines
4.7 KiB
Rust
80 lines
4.7 KiB
Rust
//! Resources: `note://` listing, reading, paging and the paths outside the vault.
|
|
|
|
mod common;
|
|
|
|
use common::ask;
|
|
use notescraft_mcp::{NOTE_SCHEME, PAGE_LIMIT};
|
|
use serde_json::{Value, json};
|
|
|
|
/// The resources of a `resources/list` reply.
|
|
fn listed(reply: &Value) -> &Vec<Value> {
|
|
reply.get("result").and_then(|result| result.get("resources")).and_then(Value::as_array).unwrap_or_else(|| panic!("a page of resources: {reply}"))
|
|
}
|
|
|
|
#[test]
|
|
fn every_note_is_a_note_resource() {
|
|
let (_vault, mut server) = common::read_only_server("res-list");
|
|
let reply = ask(&mut server, &json!({"jsonrpc": "2.0", "id": 1, "method": "resources/list"}).to_string());
|
|
let resources = listed(&reply);
|
|
let uris: Vec<&str> = resources.iter().filter_map(|entry| entry.get("uri")).filter_map(Value::as_str).collect();
|
|
assert_eq!(uris, vec!["note://Alpha.md", "note://Beta.md", "note://folder/Gamma.md"], "the index order, and no attachment");
|
|
for entry in resources {
|
|
assert_eq!(entry.get("mimeType").and_then(Value::as_str), Some("text/markdown"));
|
|
assert!(entry.get("name").and_then(Value::as_str).is_some());
|
|
}
|
|
assert!(reply.get("result").and_then(|result| result.get("nextCursor")).is_none(), "three notes fit in one page");
|
|
}
|
|
|
|
#[test]
|
|
fn a_page_cursor_walks_the_whole_list() {
|
|
let (_vault, mut server) = common::server_with_extra("res-page", PAGE_LIMIT + 5);
|
|
let first = ask(&mut server, &json!({"jsonrpc": "2.0", "id": 1, "method": "resources/list"}).to_string());
|
|
assert_eq!(listed(&first).len(), PAGE_LIMIT);
|
|
let cursor = first.get("result").and_then(|result| result.get("nextCursor")).and_then(Value::as_str).expect("a cursor").to_owned();
|
|
let second = ask(&mut server, &json!({"jsonrpc": "2.0", "id": 2, "method": "resources/list", "params": {"cursor": cursor}}).to_string());
|
|
assert_eq!(listed(&second).len(), 8, "the three fixtures plus the five extras past the cap");
|
|
assert!(second.get("result").and_then(|result| result.get("nextCursor")).is_none(), "the last page has no cursor");
|
|
}
|
|
|
|
#[test]
|
|
fn a_bad_cursor_is_an_invalid_params_error() {
|
|
let (_vault, mut server) = common::read_only_server("res-cursor");
|
|
for cursor in ["abc", "", "-1", "99999999999999999999999999"] {
|
|
let reply = ask(&mut server, &json!({ "jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": { "cursor": cursor } }).to_string());
|
|
assert_eq!(reply.get("error").and_then(|error| error.get("code")).and_then(Value::as_i64), Some(-32602), "cursor {cursor:?}: {reply}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_note_reads_back_through_the_engine() {
|
|
let (_vault, mut server) = common::read_only_server("res-read");
|
|
let reply = ask(&mut server, &json!({"jsonrpc": "2.0", "id": 1, "method": "resources/read", "params": {"uri": "note://folder/Gamma.md"}}).to_string());
|
|
let contents = reply.get("result").and_then(|result| result.get("contents")).and_then(Value::as_array).cloned().unwrap_or_default();
|
|
assert_eq!(contents.len(), 1);
|
|
let first = contents.first().expect("one content");
|
|
assert_eq!(first.get("uri").and_then(Value::as_str), Some("note://folder/Gamma.md"));
|
|
assert_eq!(first.get("mimeType").and_then(Value::as_str), Some("text/markdown"));
|
|
assert!(first.get("text").and_then(Value::as_str).is_some_and(|text| text.contains("# Gamma")), "{first}");
|
|
let _ = NOTE_SCHEME;
|
|
}
|
|
|
|
#[test]
|
|
fn a_uri_outside_the_vault_or_of_another_scheme_is_refused() {
|
|
let (_vault, mut server) = common::read_only_server("res-outside");
|
|
for uri in ["note://../escape.md", "note:///etc/passwd", "note://.obsidian/config.md", "file://Alpha.md", "note://", "note://img.png", "42"] {
|
|
let reply = ask(&mut server, &json!({ "jsonrpc": "2.0", "id": 1, "method": "resources/read", "params": { "uri": uri } }).to_string());
|
|
assert!(reply.get("error").and_then(|error| error.get("code")).and_then(Value::as_i64) == Some(-32602), "{uri} must be refused with -32602: {reply}");
|
|
}
|
|
let no_uri = ask(&mut server, &json!({ "jsonrpc": "2.0", "id": 1, "method": "resources/read", "params": {} }).to_string());
|
|
assert_eq!(no_uri.get("error").and_then(|error| error.get("code")).and_then(Value::as_i64), Some(-32602));
|
|
let bad_params = ask(&mut server, &json!({ "jsonrpc": "2.0", "id": 1, "method": "resources/read", "params": [] }).to_string());
|
|
assert_eq!(bad_params.get("error").and_then(|error| error.get("code")).and_then(Value::as_i64), Some(-32602));
|
|
}
|
|
|
|
#[test]
|
|
fn there_are_no_resource_templates() {
|
|
let (_vault, mut server) = common::read_only_server("res-templates");
|
|
let reply = ask(&mut server, &json!({"jsonrpc": "2.0", "id": 1, "method": "resources/templates/list"}).to_string());
|
|
assert_eq!(reply.get("result").and_then(|result| result.get("resourceTemplates")).and_then(Value::as_array).map(Vec::len), Some(0), "{reply}");
|
|
}
|