Files
Marcos Paulo 50e6ce5ed0 feat(mcp): wave 8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 00:00:21 -03:00

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