173 lines
7.3 KiB
Rust
173 lines
7.3 KiB
Rust
//! The advanced URI surface: the `notescraft://` links and the `uri.resolve`
|
|
//! command.
|
|
|
|
mod common;
|
|
|
|
use common::{seed, temp_vault};
|
|
use notescraft_engine::uri::{SCHEME, UriAction, format_open_uri, parse};
|
|
use notescraft_engine::{Error, Registry, Session, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
/// The vault name of a temp vault: the folder name, as the UI shows it.
|
|
fn vault_name(vault: &common::TempVault) -> String {
|
|
vault.path().file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default()
|
|
}
|
|
|
|
/// A registry with the core commands and a session on a small vault.
|
|
fn fixture(name: &str) -> (Registry<Session>, Session, common::TempVault) {
|
|
let vault = temp_vault(name);
|
|
seed(vault.path(), &[("root.md", "# Root\n"), ("notes/Child.md", "# Child\n")]);
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register the core commands");
|
|
let session = Session::open(vault.path()).expect("open the vault");
|
|
(reg, session, vault)
|
|
}
|
|
|
|
/// The error code a command reported.
|
|
fn code(result: &Result<Value, Error>) -> String {
|
|
match result {
|
|
Err(Error::Command { source, .. }) => source.code.clone(),
|
|
other => panic!("expected a command error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// Every action of the grammar is parsed.
|
|
#[test]
|
|
fn every_action_is_parsed() {
|
|
let open = parse("notescraft://open?vault=My%20Vault&file=notes/Child.md&heading=Child&block=%5Eabc&line=12").expect("open");
|
|
assert_eq!(
|
|
open,
|
|
UriAction::Open {
|
|
vault: "My Vault".to_owned(),
|
|
file: "notes/Child.md".to_owned(),
|
|
heading: Some("Child".to_owned()),
|
|
block: Some("^abc".to_owned()),
|
|
line: Some(12),
|
|
}
|
|
);
|
|
|
|
let new = parse("notescraft://new?vault=V&name=Inbox/New.md&content=Hello%20world").expect("new");
|
|
assert_eq!(new, UriAction::New { vault: "V".to_owned(), name: "Inbox/New.md".to_owned(), content: "Hello world".to_owned() });
|
|
|
|
let daily = parse("notescraft://daily?vault=V").expect("daily");
|
|
assert_eq!(daily, UriAction::Daily { vault: "V".to_owned() });
|
|
|
|
let search = parse("notescraft://search?vault=V&query=rust").expect("search");
|
|
assert_eq!(search, UriAction::Search { vault: "V".to_owned(), query: "rust".to_owned() });
|
|
}
|
|
|
|
/// Percent-encoded UTF-8 comes back as it was written.
|
|
#[test]
|
|
fn percent_encoded_utf8_is_decoded() {
|
|
let action = parse("notescraft://open?vault=Notes&file=Di%C3%A1rios%2FA%C3%A7%C3%A3o.md").expect("open");
|
|
assert_eq!(
|
|
action,
|
|
UriAction::Open { vault: "Notes".to_owned(), file: "Di\u{e1}rios/A\u{e7}\u{e3}o.md".to_owned(), heading: None, block: None, line: None }
|
|
);
|
|
|
|
let search = parse("notescraft://search?vault=Notes&query=a+b").expect("search");
|
|
assert_eq!(search, UriAction::Search { vault: "Notes".to_owned(), query: "a+b".to_owned() }, "a plus is not a space");
|
|
}
|
|
|
|
/// The paths a link may not carry are refused.
|
|
#[test]
|
|
fn hostile_paths_are_refused() {
|
|
for url in [
|
|
"notescraft://open?vault=V&file=../escape.md",
|
|
"notescraft://open?vault=V&file=a/../../escape.md",
|
|
"notescraft://open?vault=V&file=/etc/passwd.md",
|
|
"notescraft://open?vault=V&file=.obsidian/app.md",
|
|
"notescraft://open?vault=V&file=.notescraft/x.md",
|
|
"notescraft://new?vault=V&name=.notescraft/x.md",
|
|
"notescraft://open?vault=V&file=root%00.md",
|
|
"notescraft://open?vault=V&file=root.txt",
|
|
] {
|
|
let error = parse(url).expect_err("the link must be refused");
|
|
assert_eq!(error.code, "invalid_params", "{url}");
|
|
}
|
|
}
|
|
|
|
/// A link over the cap is refused before it is looked at.
|
|
#[test]
|
|
fn a_long_link_is_refused() {
|
|
let url = format!("notescraft://open?vault=V&file={}.md", "a".repeat(5000));
|
|
let error = parse(&url).expect_err("the link must be refused");
|
|
assert_eq!(error.code, "invalid_params");
|
|
}
|
|
|
|
/// Only the four actions are supported; a link never runs a command.
|
|
#[test]
|
|
fn an_unknown_action_is_refused() {
|
|
for url in ["notescraft://delete?vault=V", "notescraft://", "notescraft://open?vault=V&file=a.md&typo=1", "https://example.com"] {
|
|
assert!(parse(url).is_err(), "{url}");
|
|
}
|
|
let error = parse("notescraft://command?id=note.write").expect_err("a link never runs a command");
|
|
assert_eq!(error.code, "invalid_params");
|
|
assert!(error.message.contains("command"), "{}", error.message);
|
|
}
|
|
|
|
/// The command reports the note the link points at.
|
|
#[test]
|
|
fn the_resolve_command_reports_the_note() {
|
|
let (reg, mut session, vault) = fixture("resolve");
|
|
let name = vault_name(&vault);
|
|
|
|
let resolved = reg.run("uri.resolve", &mut session, &json!({ "url": format_open_uri(&name, "root.md", None) })).expect("uri.resolve");
|
|
assert_eq!(resolved.get("action"), Some(&json!("open")));
|
|
assert_eq!(resolved.get("vault"), Some(&json!(name)));
|
|
assert_eq!(resolved.get("path"), Some(&json!("root.md")));
|
|
assert_eq!(resolved.get("exists"), Some(&json!(true)));
|
|
assert_eq!(resolved.get("needs_confirmation"), Some(&json!(false)));
|
|
|
|
let missing = reg.run("uri.resolve", &mut session, &json!({ "url": format_open_uri(&name, "nope.md", None) })).expect("uri.resolve");
|
|
assert_eq!(missing.get("exists"), Some(&json!(false)));
|
|
}
|
|
|
|
/// A vault that is not open is not found.
|
|
#[test]
|
|
fn an_unknown_vault_is_not_found() {
|
|
let (reg, mut session, _vault) = fixture("unknown-vault");
|
|
let result = reg.run("uri.resolve", &mut session, &json!({ "url": "notescraft://open?vault=other&file=root.md" }));
|
|
assert_eq!(code(&result), "not_found");
|
|
}
|
|
|
|
/// A link that would create a note asks first, and writes nothing.
|
|
#[test]
|
|
fn a_new_link_asks_for_confirmation() {
|
|
let (reg, mut session, vault) = fixture("new-link");
|
|
let name = vault_name(&vault);
|
|
let url = format!("{SCHEME}new?vault={name}&name=Inbox/New.md&content=Hello");
|
|
|
|
let resolved = reg.run("uri.resolve", &mut session, &json!({ "url": url })).expect("uri.resolve");
|
|
assert_eq!(resolved.get("action"), Some(&json!("new")));
|
|
assert_eq!(resolved.get("path"), Some(&json!("Inbox/New.md")));
|
|
assert_eq!(resolved.get("needs_confirmation"), Some(&json!(true)));
|
|
assert_eq!(resolved.get("content"), Some(&json!("Hello")));
|
|
assert!(!vault.path().join("Inbox/New.md").exists(), "the link wrote nothing");
|
|
}
|
|
|
|
/// A daily link reports the note of today.
|
|
#[test]
|
|
fn a_daily_link_reports_todays_note() {
|
|
let (reg, mut session, vault) = fixture("daily");
|
|
let name = vault_name(&vault);
|
|
|
|
let resolved = reg.run("uri.resolve", &mut session, &json!({ "url": format!("{SCHEME}daily?vault={name}") })).expect("uri.resolve");
|
|
assert_eq!(resolved.get("action"), Some(&json!("daily")));
|
|
let path = resolved.get("path").and_then(Value::as_str).unwrap_or_default();
|
|
assert!(path.starts_with("Daily/"), "{path}");
|
|
assert!(path.to_lowercase().ends_with(".md"), "{path}");
|
|
}
|
|
|
|
/// The helper the UI copies round trips through the parser.
|
|
#[test]
|
|
fn the_format_helper_round_trips() {
|
|
let url = format_open_uri("My Vault", "a/b c.md", Some("Head ing"));
|
|
assert_eq!(url, "notescraft://open?vault=My%20Vault&file=a%2Fb%20c.md&heading=Head%20ing");
|
|
assert_eq!(
|
|
parse(&url).expect("the link"),
|
|
UriAction::Open { vault: "My Vault".to_owned(), file: "a/b c.md".to_owned(), heading: Some("Head ing".to_owned()), block: None, line: None }
|
|
);
|
|
assert_eq!(format_open_uri("V", "a.md", None), "notescraft://open?vault=V&file=a.md");
|
|
}
|